-
Notifications
You must be signed in to change notification settings - Fork 636
Move server-side apply function to a shared package #4297
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,6 @@ import ( | |
| "k8s.io/apimachinery/pkg/api/meta" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apimachinery/pkg/util/rand" | ||
| "k8s.io/apimachinery/pkg/util/version" | ||
| "k8s.io/client-go/tools/record" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/reconcile" | ||
|
|
@@ -342,59 +341,32 @@ spec: | |
| // | ||
| // The "metadata.finalizers" field is also okay. | ||
| // - https://book.kubebuilder.io/reference/using-finalizers.html | ||
| // | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Highly recommend turning off whitespace differences to see what's actually going on here for anyone reviewing this |
||
| // NOTE(cbandy): Kubernetes prior to v1.16.10 and v1.17.6 does not track | ||
| // managed fields on the status subresource: https://issue.k8s.io/88901 | ||
| switch { | ||
| case suite.ServerVersion.LessThan(version.MustParseGeneric("1.22")): | ||
|
|
||
| // Kubernetes 1.22 began tracking subresources in managed fields. | ||
| // - https://pr.k8s.io/100970 | ||
| Expect(existing.ManagedFields).To(ContainElement( | ||
| MatchFields(IgnoreExtras, Fields{ | ||
| "Manager": Equal(test.Owner), | ||
| "FieldsV1": PointTo(MatchAllFields(Fields{ | ||
| "Raw": WithTransform(func(in []byte) (out map[string]any) { | ||
| Expect(yaml.Unmarshal(in, &out)).To(Succeed()) | ||
| return out | ||
| }, MatchAllKeys(Keys{ | ||
| "f:metadata": MatchAllKeys(Keys{ | ||
| "f:finalizers": Not(BeZero()), | ||
| }), | ||
| "f:status": Not(BeZero()), | ||
| })), | ||
| })), | ||
| }), | ||
| ), `controller should manage only "finalizers" and "status"`) | ||
|
|
||
| default: | ||
| Expect(existing.ManagedFields).To(ContainElements( | ||
| MatchFields(IgnoreExtras, Fields{ | ||
| "Manager": Equal(test.Owner), | ||
| "FieldsV1": PointTo(MatchAllFields(Fields{ | ||
| "Raw": WithTransform(func(in []byte) (out map[string]any) { | ||
| Expect(yaml.Unmarshal(in, &out)).To(Succeed()) | ||
| return out | ||
| }, MatchAllKeys(Keys{ | ||
| "f:metadata": MatchAllKeys(Keys{ | ||
| "f:finalizers": Not(BeZero()), | ||
| }), | ||
| })), | ||
| Expect(existing.ManagedFields).To(ContainElements( | ||
| MatchFields(IgnoreExtras, Fields{ | ||
| "Manager": Equal(test.Owner), | ||
| "FieldsV1": PointTo(MatchAllFields(Fields{ | ||
| "Raw": WithTransform(func(in []byte) (out map[string]any) { | ||
| Expect(yaml.Unmarshal(in, &out)).To(Succeed()) | ||
| return out | ||
| }, MatchAllKeys(Keys{ | ||
| "f:metadata": MatchAllKeys(Keys{ | ||
| "f:finalizers": Not(BeZero()), | ||
| }), | ||
| })), | ||
| }), | ||
| MatchFields(IgnoreExtras, Fields{ | ||
| "Manager": Equal(test.Owner), | ||
| "FieldsV1": PointTo(MatchAllFields(Fields{ | ||
| "Raw": WithTransform(func(in []byte) (out map[string]any) { | ||
| Expect(yaml.Unmarshal(in, &out)).To(Succeed()) | ||
| return out | ||
| }, MatchAllKeys(Keys{ | ||
| "f:status": Not(BeZero()), | ||
| })), | ||
| })), | ||
| }), | ||
| MatchFields(IgnoreExtras, Fields{ | ||
| "Manager": Equal(test.Owner), | ||
| "FieldsV1": PointTo(MatchAllFields(Fields{ | ||
| "Raw": WithTransform(func(in []byte) (out map[string]any) { | ||
| Expect(yaml.Unmarshal(in, &out)).To(Succeed()) | ||
| return out | ||
| }, MatchAllKeys(Keys{ | ||
| "f:status": Not(BeZero()), | ||
| })), | ||
| }), | ||
| ), `controller should manage only "finalizers" and "status"`) | ||
| } | ||
| })), | ||
| }), | ||
| ), `controller should manage only "finalizers" and "status"`) | ||
| }) | ||
|
|
||
| Specify("Patroni Distributed Configuration", func() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,30 +6,19 @@ package postgrescluster | |
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| . "github.com/onsi/ginkgo/v2" | ||
| . "github.com/onsi/gomega" | ||
| "k8s.io/apimachinery/pkg/util/version" | ||
| "k8s.io/client-go/discovery" | ||
| "k8s.io/client-go/rest" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| "sigs.k8s.io/controller-runtime/pkg/log" | ||
| "sigs.k8s.io/controller-runtime/pkg/manager" | ||
|
|
||
| "github.com/crunchydata/postgres-operator/internal/logging" | ||
| "github.com/crunchydata/postgres-operator/internal/testing/require" | ||
| ) | ||
|
|
||
| var suite struct { | ||
| Client client.Client | ||
| Config *rest.Config | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, the only use of ServerVersion was in the test in |
||
|
|
||
| ServerVersion *version.Version | ||
|
|
||
| Manager manager.Manager | ||
| } | ||
|
|
||
| func TestAPIs(t *testing.T) { | ||
|
|
@@ -39,24 +28,10 @@ func TestAPIs(t *testing.T) { | |
| } | ||
|
|
||
| var _ = BeforeSuite(func() { | ||
| if os.Getenv("KUBEBUILDER_ASSETS") == "" && !strings.EqualFold(os.Getenv("USE_EXISTING_CLUSTER"), "true") { | ||
| Skip("skipping") | ||
| } | ||
| suite.Client = require.Kubernetes(GinkgoT()) | ||
|
|
||
| logging.SetLogSink(logging.Logrus(GinkgoWriter, "test", 1, 1)) | ||
| log.SetLogger(logging.FromContext(context.Background())) | ||
|
|
||
| By("bootstrapping test environment") | ||
| suite.Config, suite.Client = require.Kubernetes2(GinkgoT()) | ||
|
|
||
| dc, err := discovery.NewDiscoveryClientForConfig(suite.Config) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| server, err := dc.ServerVersion() | ||
| Expect(err).ToNot(HaveOccurred()) | ||
|
|
||
| suite.ServerVersion, err = version.ParseGeneric(server.GitVersion) | ||
| Expect(err).ToNot(HaveOccurred()) | ||
| }) | ||
|
|
||
| var _ = AfterSuite(func() { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| // Copyright 2021 - 2025 Crunchy Data Solutions, Inc. | ||
| // | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package runtime | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| corev1 "k8s.io/api/core/v1" | ||
| "k8s.io/apimachinery/pkg/api/equality" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| ) | ||
|
|
||
| // Apply sends an apply patch with force=true using cc and updates object with any returned content. | ||
| // The client is responsible for setting fieldManager; see [client.WithFieldOwner]. | ||
| // | ||
| // - https://docs.k8s.io/reference/using-api/server-side-apply#managers | ||
| // - https://docs.k8s.io/reference/using-api/server-side-apply#conflicts | ||
| func Apply[ | ||
| // NOTE: This interface can go away following https://go.dev/issue/47487. | ||
| ClientPatch interface { | ||
| Patch(context.Context, client.Object, client.Patch, ...client.PatchOption) error | ||
| }, | ||
| T interface{ client.Object }, | ||
| ](ctx context.Context, cc ClientPatch, object T) error { | ||
| // Generate an apply-patch by comparing the object to its zero value. | ||
| data, err := client.MergeFrom(*new(T)).Data(object) | ||
| apply := client.RawPatch(client.Apply.Type(), data) | ||
|
|
||
| // Keep a copy of the object before any API calls. | ||
| intent := object.DeepCopyObject() | ||
|
|
||
| // Send the apply-patch with force=true. | ||
| if err == nil { | ||
| err = cc.Patch(ctx, object, apply, client.ForceOwnership) | ||
| } | ||
|
|
||
| // Some fields cannot be server-side applied correctly. | ||
| // When their outcome does not match the intent, send a json-patch to get really specific. | ||
| patch := NewJSONPatch() | ||
|
|
||
| switch actual := any(object).(type) { | ||
| case *corev1.Service: | ||
| intent := intent.(*corev1.Service) | ||
|
|
||
| // Service.Spec.Selector cannot be unset; perhaps https://issue.k8s.io/117447 | ||
| if !equality.Semantic.DeepEqual(actual.Spec.Selector, intent.Spec.Selector) { | ||
| patch.Replace("spec", "selector")(intent.Spec.Selector) | ||
| } | ||
| } | ||
|
|
||
| // Send the json-patch when necessary. | ||
| if err == nil && !patch.IsEmpty() { | ||
| err = cc.Patch(ctx, object, patch) | ||
| } | ||
| return err | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you thinking about a separate PR to remove this file and change the many
applys in internal/controller/postgrescluster?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't planning on it, but that seems reasonable. I skipped it here because it is called a lot.