diff --git a/transform/kubernetes/kubernetes.go b/transform/kubernetes/kubernetes.go index 9561b1f..d175d3a 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"}, } @@ -569,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) { @@ -585,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 e5ddaaf..06df960 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~1last-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~1last-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~1last-applied-configuration"},{"op": "remove", "path": "/spec/ports/1/nodePort"}]`, }, }