From 2be624cfa76759cf489ea77e0a7c3b000ddca58f Mon Sep 17 00:00:00 2001 From: Marek Aufart Date: Mon, 13 Apr 2026 10:45:48 +0200 Subject: [PATCH 1/2] Remove last-applied-configuration from annotations Current exported data contains last-applied-configuration annotation, that is not needed for migration, so should be removed. Related to https://github.com/migtools/crane/issues/253 (also more details there) Signed-off-by: Marek Aufart --- transform/kubernetes/kubernetes.go | 1 + transform/kubernetes/kubernetes_test.go | 26 +++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/transform/kubernetes/kubernetes.go b/transform/kubernetes/kubernetes.go index 9561b1f..b0d872b 100644 --- a/transform/kubernetes/kubernetes.go +++ b/transform/kubernetes/kubernetes.go @@ -73,6 +73,7 @@ var fieldsToStrip = [...][]string{ {metadata, "creationTimestamp"}, {metadata, "generation"}, {metadata, "managedFields"}, + {metadata, "annotations", "kubectl.kubernetes.io/last-applied-configuration"}, {"status"}, } diff --git a/transform/kubernetes/kubernetes_test.go b/transform/kubernetes/kubernetes_test.go index e5ddaaf..34489da 100644 --- a/transform/kubernetes/kubernetes_test.go +++ b/transform/kubernetes/kubernetes_test.go @@ -446,6 +446,28 @@ func TestRun(t *testing.T) { "multiple-testing", }, }, + { + Name: "RemoveLastAppliedConfigurationAnnotation", + Object: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "kind": "Deployment", + "apiVersion": "apps/v1", + "metadata": map[string]interface{}{ + "name": "test-deployment", + "namespace": "default", + "annotations": map[string]interface{}{ + "kubectl.kubernetes.io/last-applied-configuration": `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment"}}`, + "other-annotation": "keep-this", + }, + }, + }, + }, + Response: transform.PluginResponse{ + IsWhiteOut: false, + Version: "v1", + }, + PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io/last-applied-configuration"}]`, + }, { Name: "HandlePod", Object: &unstructured.Unstructured{ @@ -619,7 +641,7 @@ func TestRun(t *testing.T) { IsWhiteOut: false, Version: "v1", }, - PatchResponseJson: `[{"op": "remove", "path": "/spec/ports/0/nodePort"}]`, + PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io/last-applied-configuration"},{"op": "remove", "path": "/spec/ports/0/nodePort"}]`, }, { Name: "HandleNodePortNamedAnnotation", @@ -656,7 +678,7 @@ func TestRun(t *testing.T) { IsWhiteOut: false, Version: "v1", }, - PatchResponseJson: `[{"op": "remove", "path": "/spec/ports/1/nodePort"}]`, + PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io/last-applied-configuration"},{"op": "remove", "path": "/spec/ports/1/nodePort"}]`, }, } From b0df3f121d6fc395861bfbda3fc55dbdb573ef5c Mon Sep 17 00:00:00 2001 From: Marek Aufart Date: Tue, 14 Apr 2026 10:35:43 +0200 Subject: [PATCH 2/2] Update jsonpatch parsing and escaping Signed-off-by: Marek Aufart --- transform/kubernetes/kubernetes.go | 21 ++++++++++++++------- transform/kubernetes/kubernetes_test.go | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/transform/kubernetes/kubernetes.go b/transform/kubernetes/kubernetes.go index b0d872b..d175d3a 100644 --- a/transform/kubernetes/kubernetes.go +++ b/transform/kubernetes/kubernetes.go @@ -570,12 +570,13 @@ func (k *KubernetesTransformPlugin) getKubernetesTransforms(obj unstructured.Uns return jsonPatch, nil } -func interfaceSlice(inStrings []string) []interface{} { - var outSlice []interface{} - for _, str := range inStrings { - outSlice = append(outSlice, str) - } - return outSlice +// escapeJSONPointer escapes a string for use in a JSON Pointer path according to RFC 6901 +// ~ must be escaped as ~0 +// / must be escaped as ~1 +func escapeJSONPointer(s string) string { + s = strings.ReplaceAll(s, "~", "~0") + s = strings.ReplaceAll(s, "/", "~1") + return s } func stripFields(obj unstructured.Unstructured) (jsonpatch.Patch, error) { @@ -586,7 +587,13 @@ func stripFields(obj unstructured.Unstructured) (jsonpatch.Patch, error) { return patches, err } if found { - patch, err := jsonpatch.DecodePatch([]byte(fmt.Sprintf(opRemove, fmt.Sprintf(strings.Repeat("/%v", len(field)), interfaceSlice(field)...)))) + // Build the JSON Pointer path with proper escaping + var pathParts []string + for _, f := range field { + pathParts = append(pathParts, escapeJSONPointer(f)) + } + path := "/" + strings.Join(pathParts, "/") + patch, err := jsonpatch.DecodePatch([]byte(fmt.Sprintf(opRemove, path))) if err != nil { return nil, err } diff --git a/transform/kubernetes/kubernetes_test.go b/transform/kubernetes/kubernetes_test.go index 34489da..06df960 100644 --- a/transform/kubernetes/kubernetes_test.go +++ b/transform/kubernetes/kubernetes_test.go @@ -466,7 +466,7 @@ func TestRun(t *testing.T) { IsWhiteOut: false, Version: "v1", }, - PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io/last-applied-configuration"}]`, + PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"}]`, }, { Name: "HandlePod", @@ -641,7 +641,7 @@ func TestRun(t *testing.T) { IsWhiteOut: false, Version: "v1", }, - PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io/last-applied-configuration"},{"op": "remove", "path": "/spec/ports/0/nodePort"}]`, + PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"},{"op": "remove", "path": "/spec/ports/0/nodePort"}]`, }, { Name: "HandleNodePortNamedAnnotation", @@ -678,7 +678,7 @@ func TestRun(t *testing.T) { IsWhiteOut: false, Version: "v1", }, - PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io/last-applied-configuration"},{"op": "remove", "path": "/spec/ports/1/nodePort"}]`, + PatchResponseJson: `[{"op":"remove","path":"/metadata/annotations/kubectl.kubernetes.io~1last-applied-configuration"},{"op": "remove", "path": "/spec/ports/1/nodePort"}]`, }, }