From fc15c11a0893e3d4c1395d11ae154386d24ca253 Mon Sep 17 00:00:00 2001 From: Austin Abro Date: Fri, 14 Nov 2025 13:21:44 -0500 Subject: [PATCH] fix ssa ownership Signed-off-by: Austin Abro --- pkg/kube/client.go | 7 +++++ pkg/kube/client_test.go | 68 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 68f1e6475..a3c731249 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1101,6 +1101,13 @@ func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts WithFieldManager(getManagedFieldsManager()). WithFieldValidation(string(fieldValidationDirective)) + // Kubernetes looks at the server side state when evaluating conflicts in field manager rather than the field manager field in the request + // therefore when using the Apply operation you cannot define managedFields in the body of the request that you submit + // This is according to https://kubernetes.io/docs/reference/using-api/server-side-apply/#apply-and-update + if u, ok := target.Object.(*unstructured.Unstructured); ok { + u.SetManagedFields(nil) + } + // Send the full object to be applied on the server side. data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, target.Object) if err != nil { diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index d49e179e0..d14000e4f 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -2183,3 +2183,71 @@ status: require.Error(t, err) assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) } + +func TestPatchResourceServerSide_ClearsManagedFields(t *testing.T) { + // Test that managedFields are cleared before sending SSA request + podWithManagedFields := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]interface{}{ + "name": "test-pod", + "namespace": "default", + "managedFields": []interface{}{ + map[string]interface{}{ + "manager": "kubectl", + "operation": "Apply", + "apiVersion": "v1", + }, + }, + }, + "spec": map[string]interface{}{ + "containers": []interface{}{ + map[string]interface{}{ + "name": "test", + "image": "nginx", + }, + }, + }, + }, + } + + // Create a fake REST client that will receive the patch request + var patchedData []byte + fakeClient := &fake.RESTClient{ + GroupVersion: schema.GroupVersion{Version: "v1"}, + NegotiatedSerializer: scheme.Codecs.WithoutConversion(), + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + patchedData = body + + header := http.Header{} + header.Set("Content-Type", runtime.ContentTypeJSON) + return &http.Response{ + StatusCode: http.StatusOK, + Header: header, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil + }), + } + + target := &resource.Info{ + Name: "test-pod", + Namespace: "default", + Object: podWithManagedFields, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Version: "v1", Resource: "pods"}, + GroupVersionKind: schema.GroupVersionKind{Version: "v1", Kind: "Pod"}, + Scope: meta.RESTScopeNamespace, + }, + Client: fakeClient, + } + + // Call patchResourceServerSide + err := patchResourceServerSide(target, false, false, FieldValidationDirectiveStrict) + require.NoError(t, err) + + // Verify the patched data doesn't contain managedFields + assert.NotContains(t, string(patchedData), "managedFields", "patched data should not contain managedFields") +}