Skip to content

Commit b2605ab

Browse files
authored
Merge pull request #6880 from sbueringer/pr-optimize-ssa-dryrun
🐛 SSA: ignore diff of other managers
2 parents 05f10fd + 0528eda commit b2605ab

File tree

3 files changed

+58
-24
lines changed

3 files changed

+58
-24
lines changed

internal/controllers/topology/cluster/structuredmerge/dryrun.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,26 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
6363
}
6464

6565
// Cleanup the dryRunUnstructured object to remove the added TopologyDryRunAnnotation
66-
// and remove the affected managedFields for `manager=capi-topology` which would
66+
// and remove the affected managedFields for `manager=capi-topology` which would
6767
// otherwise show the additional field ownership for the annotation we added and
6868
// the changed managedField timestamp.
69-
if err := cleanupTopologyDryRunAnnotation(dryRunCtx.dryRunUnstructured); err != nil {
69+
// We also drop managedFields of other managers as we don't care about if other managers
70+
// made changes to the object. We only want to trigger a ServerSideApply if we would make
71+
// changes to the object.
72+
// Please note that if other managers made changes to fields that we care about and thus ownership changed,
73+
// this would affect our managed fields as well and we would still detect it by diffing our managed fields.
74+
if err := cleanupManagedFieldsAndAnnotation(dryRunCtx.dryRunUnstructured); err != nil {
7075
return false, false, errors.Wrap(err, "failed to filter topology dry-run annotation on dryRunUnstructured")
7176
}
77+
7278
// Also run the function for the originalUnstructured to remove the managedField
7379
// timestamp for `manager=capi-topology`.
74-
if err := cleanupTopologyDryRunAnnotation(dryRunCtx.originalUnstructured); err != nil {
80+
// We also drop managedFields of other managers as we don't care about if other managers
81+
// made changes to the object. We only want to trigger a ServerSideApply if we would make
82+
// changes to the object.
83+
// Please note that if other managers made changes to fields that we care about and thus ownership changed,
84+
// this would affect our managed fields as well and we would still detect it by diffing our managed fields.
85+
if err := cleanupManagedFieldsAndAnnotation(dryRunCtx.originalUnstructured); err != nil {
7586
return false, false, errors.Wrap(err, "failed to filter topology dry-run annotation on originalUnstructured")
7687
}
7788

@@ -106,11 +117,11 @@ func dryRunSSAPatch(ctx context.Context, dryRunCtx *dryRunSSAPatchInput) (bool,
106117
return hasChanges, hasSpecChanges, nil
107118
}
108119

109-
// cleanupTopologyDryRunAnnotation adjusts the obj to remove the topology.cluster.x-k8s.io/dry-run
120+
// cleanupManagedFieldsAndAnnotation adjusts the obj to remove the topology.cluster.x-k8s.io/dry-run
110121
// annotation as well as the field ownership reference in managedFields. It does
111122
// also remove the timestamp of the managedField for `manager=capi-topology` because
112123
// it is expected to change due to the additional annotation.
113-
func cleanupTopologyDryRunAnnotation(obj *unstructured.Unstructured) error {
124+
func cleanupManagedFieldsAndAnnotation(obj *unstructured.Unstructured) error {
114125
// Filter the topology.cluster.x-k8s.io/dry-run annotation as well as leftover empty maps.
115126
filterIntent(&filterIntentInput{
116127
path: contract.Path{},
@@ -120,9 +131,11 @@ func cleanupTopologyDryRunAnnotation(obj *unstructured.Unstructured) error {
120131
}),
121132
})
122133

123-
// Adjust the managed field for Manager=TopologyManagerName, Subresource="", Operation="Apply"
124-
managedFields := obj.GetManagedFields()
125-
for i, managedField := range managedFields {
134+
// Adjust the managed field for Manager=TopologyManagerName, Subresource="", Operation="Apply" and
135+
// drop managed fields of other controllers.
136+
oldManagedFields := obj.GetManagedFields()
137+
newManagedFields := []metav1.ManagedFieldsEntry{}
138+
for _, managedField := range oldManagedFields {
126139
if managedField.Manager != TopologyManagerName {
127140
continue
128141
}
@@ -157,10 +170,10 @@ func cleanupTopologyDryRunAnnotation(obj *unstructured.Unstructured) error {
157170
}
158171
managedField.FieldsV1.Raw = fieldsV1Raw
159172

160-
// Replace the modified managedField entry at the slice.
161-
managedFields[i] = managedField
162-
obj.SetManagedFields(managedFields)
173+
newManagedFields = append(newManagedFields, managedField)
163174
}
164175

176+
obj.SetManagedFields(newManagedFields)
177+
165178
return nil
166179
}

internal/controllers/topology/cluster/structuredmerge/dryrun_test.go

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2828
)
2929

30-
func Test_cleanupTopologyDryRunAnnotation(t *testing.T) {
30+
func Test_cleanupManagedFieldsAndAnnotation(t *testing.T) {
3131
rawManagedFieldWithAnnotation := `{"f:metadata":{"f:annotations":{"f:topology.cluster.x-k8s.io/dry-run":{}}}}`
3232
rawManagedFieldWithAnnotationSpecLabels := `{"f:metadata":{"f:annotations":{"f:topology.cluster.x-k8s.io/dry-run":{}},"f:labels":{}},"f:spec":{"f:foo":{}}}`
3333
rawManagedFieldWithSpecLabels := `{"f:metadata":{"f:labels":{}},"f:spec":{"f:foo":{}}}`
@@ -53,33 +53,30 @@ func Test_cleanupTopologyDryRunAnnotation(t *testing.T) {
5353
Build(),
5454
},
5555
{
56-
name: "managedFields: manager does not match",
56+
name: "managedFields: should drop managed fields of other manager",
5757
obj: newObjectBuilder().
5858
WithManagedFieldsEntry("other", "", metav1.ManagedFieldsOperationApply, []byte(`{}`), nil).
5959
Build(),
6060
wantErr: false,
6161
want: newObjectBuilder().
62-
WithManagedFieldsEntry("other", "", metav1.ManagedFieldsOperationApply, []byte(`{}`), nil).
6362
Build(),
6463
},
6564
{
66-
name: "managedFields: subresource does not match",
65+
name: "managedFields: should drop managed fields of a subresource",
6766
obj: newObjectBuilder().
6867
WithManagedFieldsEntry(TopologyManagerName, "status", metav1.ManagedFieldsOperationApply, []byte(`{}`), nil).
6968
Build(),
7069
wantErr: false,
7170
want: newObjectBuilder().
72-
WithManagedFieldsEntry(TopologyManagerName, "status", metav1.ManagedFieldsOperationApply, []byte(`{}`), nil).
7371
Build(),
7472
},
7573
{
76-
name: "managedFields: operation does not match",
74+
name: "managedFields: should drop managed fields of another operation",
7775
obj: newObjectBuilder().
7876
WithManagedFieldsEntry(TopologyManagerName, "", metav1.ManagedFieldsOperationUpdate, []byte(`{}`), nil).
7977
Build(),
8078
wantErr: false,
8179
want: newObjectBuilder().
82-
WithManagedFieldsEntry(TopologyManagerName, "", metav1.ManagedFieldsOperationUpdate, []byte(`{}`), nil).
8380
Build(),
8481
},
8582
{
@@ -93,7 +90,7 @@ func Test_cleanupTopologyDryRunAnnotation(t *testing.T) {
9390
Build(),
9491
},
9592
{
96-
name: "managedFields: cleanup the managed field entry but preserve other ownership",
93+
name: "managedFields: cleanup the managed field entry and preserve other ownership",
9794
obj: newObjectBuilder().
9895
WithManagedFieldsEntry(TopologyManagerName, "", metav1.ManagedFieldsOperationApply, []byte(rawManagedFieldWithAnnotationSpecLabels), nil).
9996
Build(),
@@ -117,8 +114,8 @@ func Test_cleanupTopologyDryRunAnnotation(t *testing.T) {
117114
t.Run(tt.name, func(t *testing.T) {
118115
g := NewWithT(t)
119116

120-
if err := cleanupTopologyDryRunAnnotation(tt.obj); (err != nil) != tt.wantErr {
121-
t.Errorf("cleanupTopologyDryRunAnnotation() error = %v, wantErr %v", err, tt.wantErr)
117+
if err := cleanupManagedFieldsAndAnnotation(tt.obj); (err != nil) != tt.wantErr {
118+
t.Errorf("cleanupManagedFieldsAndAnnotation() error = %v, wantErr %v", err, tt.wantErr)
122119
}
123120
if tt.want != nil {
124121
g.Expect(tt.obj).To(BeEquivalentTo(tt.want))
@@ -132,14 +129,26 @@ type objectBuilder struct {
132129
}
133130

134131
func newObjectBuilder() objectBuilder {
135-
return objectBuilder{&unstructured.Unstructured{Object: map[string]interface{}{}}}
132+
u := &unstructured.Unstructured{Object: map[string]interface{}{}}
133+
u.SetName("test")
134+
u.SetNamespace("default")
135+
136+
return objectBuilder{
137+
u: u,
138+
}
136139
}
137140

138141
func (b objectBuilder) DeepCopy() objectBuilder {
139142
return objectBuilder{b.u.DeepCopy()}
140143
}
141144

142145
func (b objectBuilder) Build() *unstructured.Unstructured {
146+
// Setting an empty managed field array if no managed field is set so there is
147+
// no difference between an object which never had a managed field and one from
148+
// which all managed field entries were removed.
149+
if b.u.GetManagedFields() == nil {
150+
b.u.SetManagedFields([]metav1.ManagedFieldsEntry{})
151+
}
143152
return b.u.DeepCopy()
144153
}
145154

internal/controllers/topology/cluster/structuredmerge/serversidepathhelper_test.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ func TestServerSideApply(t *testing.T) {
7373
g.Expect(p0.HasChanges()).To(BeTrue())
7474
g.Expect(p0.HasSpecChanges()).To(BeTrue())
7575
})
76-
7776
t.Run("When creating an object using server side apply, it should track managed fields for the topology controller", func(t *testing.T) {
7877
g := NewWithT(t)
7978

@@ -104,7 +103,6 @@ func TestServerSideApply(t *testing.T) {
104103
g.Expect(controlPlaneEndpointFieldV1).To(HaveKey("f:host")) // topology controller should express opinions on spec.controlPlaneEndpoint.host.
105104
g.Expect(controlPlaneEndpointFieldV1).To(HaveKey("f:port")) // topology controller should express opinions on spec.controlPlaneEndpoint.port.
106105
})
107-
108106
t.Run("Server side apply patch helper detects no changes", func(t *testing.T) {
109107
g := NewWithT(t)
110108

@@ -236,6 +234,9 @@ func TestServerSideApply(t *testing.T) {
236234
obj := obj.DeepCopy()
237235
g.Expect(env.GetAPIReader().Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed())
238236

237+
// Store object before another controller applies changes.
238+
original := obj.DeepCopy()
239+
239240
// Create a patch helper like we do/recommend doing in the controllers and use it to apply some changes.
240241
p, err := patch.NewHelper(obj, env.Client)
241242
g.Expect(err).ToNot(HaveOccurred())
@@ -246,6 +247,17 @@ func TestServerSideApply(t *testing.T) {
246247
g.Expect(unstructured.SetNestedField(obj.Object, true, "status", "ready")).To(Succeed()) // Required field
247248

248249
g.Expect(p.Patch(ctx, obj)).To(Succeed())
250+
251+
// Verify that the topology controller detects no changes after another controller changed fields.
252+
// Note: We verify here that the ServerSidePatchHelper ignores changes in managed fields of other controllers.
253+
// There's also a change in .spec.bar that is intentionally not ignored by the controller, we have to ignore
254+
// it here to be able to verify that managed field changes are ignored. This is the same situation as when
255+
// other controllers update .status (that is ignored) and the ServerSidePatchHelper then ignores the corresponding
256+
// managed field changes.
257+
p0, err := NewServerSidePatchHelper(ctx, original, original, env.GetClient(), IgnorePaths{{"spec", "foo"}, {"spec", "bar"}})
258+
g.Expect(err).ToNot(HaveOccurred())
259+
g.Expect(p0.HasChanges()).To(BeFalse())
260+
g.Expect(p0.HasSpecChanges()).To(BeFalse())
249261
})
250262

251263
t.Run("Topology controller reconcile again with no changes on topology managed fields", func(t *testing.T) {

0 commit comments

Comments
 (0)