From d4062d69bce33fb66bb85dd79808f6bda3d4a25e Mon Sep 17 00:00:00 2001 From: Troy Connor Date: Thu, 7 Mar 2024 14:27:07 -0500 Subject: [PATCH] attempt at server side apply Signed-off-by: Troy Connor --- pkg/client/client.go | 16 +++++++++++++++ pkg/client/client_test.go | 25 ++++++++++++++++++++++- pkg/client/dryrun.go | 4 ++++ pkg/client/fake/client.go | 4 ++++ pkg/client/interceptor/intercept.go | 8 ++++++++ pkg/client/interceptor/intercept_test.go | 4 ++++ pkg/client/interfaces.go | 5 +++++ pkg/client/namespaced_client.go | 17 ++++++++++++++++ pkg/client/typed_client.go | 21 +++++++++++++++++++ pkg/client/unstructured_client.go | 26 ++++++++++++++++++++++++ 10 files changed, 129 insertions(+), 1 deletion(-) diff --git a/pkg/client/client.go b/pkg/client/client.go index c0ebb39e3d..85d861b9b3 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -347,6 +348,21 @@ func (c *client) Patch(ctx context.Context, obj Object, patch Patch, opts ...Pat } } +func (c *client) Apply(ctx context.Context, obj Object, fieldOwner string) error { + var err error + switch obj.(type) { + case runtime.Unstructured: + return c.Patch(ctx, obj, Apply, ForceOwnership, FieldOwner(fieldOwner)) + default: + u := &unstructured.Unstructured{} + u.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return err + } + return c.Patch(ctx, u, Apply, ForceOwnership, FieldOwner(fieldOwner)) + } +} + // Get implements client.Client. func (c *client) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { if isUncached, err := c.shouldBypassCache(obj); err != nil { diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 07d57c36ee..acc6be92df 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -795,7 +795,30 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC }) }) }) - + Describe("Server side apply", func() { + Context("with a core k8s object", func() { + It("should not error", func() { + cl, err := client.New(cfg, client.Options{}) + Expect(err).NotTo(HaveOccurred()) + Expect(cl).NotTo(BeNil()) + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "config-map", + Namespace: "default", + }, + Data: map[string]string{ + "key": "value", + }, + } + err = cl.Apply(ctx, cm, "test-client") + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) Describe("SubResourceClient", func() { Context("with structured objects", func() { It("should be able to read the Scale subresource", func() { diff --git a/pkg/client/dryrun.go b/pkg/client/dryrun.go index bbcdd38321..f2ade88ac5 100644 --- a/pkg/client/dryrun.go +++ b/pkg/client/dryrun.go @@ -82,6 +82,10 @@ func (c *dryRunClient) Patch(ctx context.Context, obj Object, patch Patch, opts return c.client.Patch(ctx, obj, patch, append(opts, DryRunAll)...) } +func (c *dryRunClient) Apply(ctx context.Context, obj Object, fieldOwner string) error { + return c.client.Apply(ctx, obj, fieldOwner) +} + // Get implements client.Client. func (c *dryRunClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { return c.client.Get(ctx, key, obj, opts...) diff --git a/pkg/client/fake/client.go b/pkg/client/fake/client.go index b90a6ebb8d..517289ba0a 100644 --- a/pkg/client/fake/client.go +++ b/pkg/client/fake/client.go @@ -801,6 +801,10 @@ func (c *fakeClient) Patch(ctx context.Context, obj client.Object, patch client. return c.patch(obj, patch, opts...) } +func (c *fakeClient) Apply(ctx context.Context, obj client.Object, fieldOwner string) error { + return c.Patch(ctx, obj, client.Apply, client.ForceOwnership, client.FieldOwner(fieldOwner)) +} + func (c *fakeClient) patch(obj client.Object, patch client.Patch, opts ...client.PatchOption) error { patchOptions := &client.PatchOptions{} patchOptions.ApplyOptions(opts) diff --git a/pkg/client/interceptor/intercept.go b/pkg/client/interceptor/intercept.go index 3d3f3cb011..92324f7f4f 100644 --- a/pkg/client/interceptor/intercept.go +++ b/pkg/client/interceptor/intercept.go @@ -19,6 +19,7 @@ type Funcs struct { DeleteAllOf func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.DeleteAllOfOption) error Update func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.UpdateOption) error Patch func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error + Apply func(ctx context.Context, client client.WithWatch, obj client.Object, fieldOwner string) error Watch func(ctx context.Context, client client.WithWatch, obj client.ObjectList, opts ...client.ListOption) (watch.Interface, error) SubResource func(client client.WithWatch, subResource string) client.SubResourceClient SubResourceGet func(ctx context.Context, client client.Client, subResourceName string, obj client.Object, subResource client.Object, opts ...client.SubResourceGetOption) error @@ -92,6 +93,13 @@ func (c interceptor) Patch(ctx context.Context, obj client.Object, patch client. return c.client.Patch(ctx, obj, patch, opts...) } +func (c interceptor) Apply(ctx context.Context, obj client.Object, fieldOwner string) error { + if c.funcs.Patch != nil { + return c.funcs.Apply(ctx, c.client, obj, fieldOwner) + } + return c.client.Apply(ctx, obj, fieldOwner) +} + func (c interceptor) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { if c.funcs.DeleteAllOf != nil { return c.funcs.DeleteAllOf(ctx, c.client, obj, opts...) diff --git a/pkg/client/interceptor/intercept_test.go b/pkg/client/interceptor/intercept_test.go index a0536789b1..fe8fb20650 100644 --- a/pkg/client/interceptor/intercept_test.go +++ b/pkg/client/interceptor/intercept_test.go @@ -360,6 +360,10 @@ func (d dummyClient) Patch(ctx context.Context, obj client.Object, patch client. return nil } +func (d dummyClient) Apply(ctx context.Context, obj client.Object, fieldOwner string) error { + return nil +} + func (d dummyClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { return nil } diff --git a/pkg/client/interfaces.go b/pkg/client/interfaces.go index 3cd745e4c0..06d72ee1e5 100644 --- a/pkg/client/interfaces.go +++ b/pkg/client/interfaces.go @@ -76,6 +76,11 @@ type Writer interface { // struct pointer so that obj can be updated with the content returned by the Server. Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error + // Apply patches the given object in the Kubernetes cluster with a server side + // apply. obj must be a struct pointer so that obj can be updated with the + // content returned by the Server + Apply(ctx context.Context, obj Object, fieldOwner string) error + // DeleteAllOf deletes all objects of the given type matching the given options. DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error } diff --git a/pkg/client/namespaced_client.go b/pkg/client/namespaced_client.go index 222dc79579..a7d87353e6 100644 --- a/pkg/client/namespaced_client.go +++ b/pkg/client/namespaced_client.go @@ -147,6 +147,23 @@ func (n *namespacedClient) Patch(ctx context.Context, obj Object, patch Patch, o return n.client.Patch(ctx, obj, patch, opts...) } +func (n *namespacedClient) Apply(ctx context.Context, obj Object, fieldOwner string) error { + isNamespaceScoped, err := n.IsObjectNamespaced(obj) + if err != nil { + return fmt.Errorf("error finding the scope of the object: %w", err) + } + + objectNamespace := obj.GetNamespace() + if objectNamespace != n.namespace && objectNamespace != "" { + return fmt.Errorf("namespace %s of the object %s does not match the namespace %s on the client", objectNamespace, obj.GetName(), n.namespace) + } + + if isNamespaceScoped && objectNamespace == "" { + obj.SetNamespace(n.namespace) + } + return n.client.Apply(ctx, obj, fieldOwner) +} + // Get implements client.Client. func (n *namespacedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { isNamespaceScoped, err := n.IsObjectNamespaced(obj) diff --git a/pkg/client/typed_client.go b/pkg/client/typed_client.go index 92afd9a9c2..f57b1ff317 100644 --- a/pkg/client/typed_client.go +++ b/pkg/client/typed_client.go @@ -20,6 +20,8 @@ import ( "context" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" ) var _ Reader = &typedClient{} @@ -132,6 +134,25 @@ func (c *typedClient) Patch(ctx context.Context, obj Object, patch Patch, opts . Into(obj) } +func (c *typedClient) Apply(ctx context.Context, obj Object, fieldOwner string) error { + o, err := c.resources.getObjMeta(obj) + if err != nil { + return err + } + + data, err := json.Marshal(o) + if err != nil { + return err + } + return o.Patch(types.ApplyPatchType). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + Body(data). + Do(ctx). + Into(obj) +} + // Get implements client.Client. func (c *typedClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { r, err := c.resources.getResource(obj) diff --git a/pkg/client/unstructured_client.go b/pkg/client/unstructured_client.go index 0d96951780..fe1208e2c3 100644 --- a/pkg/client/unstructured_client.go +++ b/pkg/client/unstructured_client.go @@ -22,6 +22,8 @@ import ( "strings" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" ) var _ Reader = &unstructuredClient{} @@ -166,6 +168,30 @@ func (uc *unstructuredClient) Patch(ctx context.Context, obj Object, patch Patch Into(obj) } +func (uc *unstructuredClient) Apply(ctx context.Context, obj Object, fieldOwner string) error { + if _, ok := obj.(runtime.Unstructured); !ok { + return fmt.Errorf("unstructured client did not understand object: %T", obj) + } + + o, err := uc.resources.getObjMeta(obj) + if err != nil { + return err + } + + data, err := json.Marshal(obj) + if err != nil { + return err + } + + return o.Patch(types.ApplyPatchType). + NamespaceIfScoped(o.GetNamespace(), o.isNamespaced()). + Resource(o.resource()). + Name(o.GetName()). + Body(data). + Do(ctx). + Into(obj) +} + // Get implements client.Client. func (uc *unstructuredClient) Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error { u, ok := obj.(runtime.Unstructured)