Skip to content

Commit 56daa46

Browse files
fix(deployment): migrate workload update calls to patch
Signed-off-by: Nazih Ben Brahim <nazihbenbrahim9@gmail.com>
1 parent 8416512 commit 56daa46

File tree

6 files changed

+281
-16
lines changed

6 files changed

+281
-16
lines changed

pkg/controller/deployment/deployment_controller.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ type DeploymentController struct {
5252
runtimeClient client.Client
5353
}
5454

55+
// patchDeploymentStatus patches the Deployment status subresource using a merge diff.
56+
// Only changed fields are sent, which avoids overwriting unknown fields from newer API versions.
57+
func (dc *DeploymentController) patchDeploymentStatus(ctx context.Context, oldD, newD *apps.Deployment) error {
58+
patch := client.MergeFrom(oldD)
59+
return dc.runtimeClient.Status().Patch(ctx, newD, patch)
60+
}
61+
5562
// getReplicaSetsForDeployment uses ControllerRefManager to reconcile
5663
// ControllerRef by adopting and orphaning.
5764
// It returns the list of ReplicaSets that this Deployment should manage.
@@ -100,10 +107,13 @@ func (dc *DeploymentController) syncDeployment(ctx context.Context, deployment *
100107
if reflect.DeepEqual(d.Spec.Selector, &everything) {
101108
dc.eventRecorder.Eventf(d, v1.EventTypeWarning, "SelectingAll", "This deployment is selecting all pods. A non-empty selector is required.")
102109
if d.Status.ObservedGeneration < d.Generation {
103-
d.Status.ObservedGeneration = d.Generation
104-
err := dc.runtimeClient.Status().Update(ctx, d)
105-
if err != nil {
106-
klog.Errorf("Failed to update deployment status: %v", err)
110+
// Use Patch instead of Update to avoid erasing unknown status fields
111+
// that a newer API server may have set (version-skew safety).
112+
dCopy := d.DeepCopy()
113+
dCopy.Status.ObservedGeneration = d.Generation
114+
if err := dc.runtimeClient.Status().Patch(ctx, dCopy, client.MergeFrom(d)); err != nil {
115+
116+
klog.Errorf("Failed to patch deployment status: %v", err)
107117
}
108118
}
109119
return nil

pkg/controller/deployment/deployment_controller_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626

2727
apps "k8s.io/api/apps/v1"
2828
"k8s.io/apimachinery/pkg/api/errors"
29+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
2931
intstrutil "k8s.io/apimachinery/pkg/util/intstr"
3032
"k8s.io/client-go/tools/record"
3133
"k8s.io/utils/pointer"
@@ -236,3 +238,39 @@ func TestSyncDeployment(t *testing.T) {
236238
})
237239
}
238240
}
241+
242+
func TestSyncDeploymentSelectingAll(t *testing.T) {
243+
deployment := generateDeployment("busybox")
244+
deployment.Spec.Selector = &metav1.LabelSelector{}
245+
deployment.Generation = 7
246+
deployment.Status.ObservedGeneration = 3
247+
deployment.Annotations = map[string]string{
248+
"test-unknown-field": "kept",
249+
}
250+
251+
fakeCtrlClient := ctrlfake.NewClientBuilder().
252+
WithStatusSubresource(&apps.Deployment{}).
253+
WithObjects(&deployment).
254+
Build()
255+
256+
dc := &DeploymentController{
257+
eventRecorder: record.NewFakeRecorder(10),
258+
runtimeClient: fakeCtrlClient,
259+
}
260+
261+
if err := dc.syncDeployment(context.TODO(), &deployment); err != nil {
262+
t.Fatalf("syncDeployment returned unexpected error: %v", err)
263+
}
264+
265+
var result apps.Deployment
266+
if err := fakeCtrlClient.Get(context.TODO(), ctrlclient.ObjectKeyFromObject(&deployment), &result); err != nil {
267+
t.Fatalf("get deployment failed: %v", err)
268+
}
269+
270+
if result.Status.ObservedGeneration != result.Generation {
271+
t.Fatalf("expected observedGeneration=%d, got %d", result.Generation, result.Status.ObservedGeneration)
272+
}
273+
if got := result.Annotations["test-unknown-field"]; got != "kept" {
274+
t.Fatalf("expected annotation test-unknown-field=kept, got %q", got)
275+
}
276+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
Copyright 2022 The Kruise Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package deployment
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
apps "k8s.io/api/apps/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"k8s.io/apimachinery/pkg/runtime"
26+
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
27+
ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
28+
)
29+
30+
type statusCallCounter struct {
31+
patchCalls int
32+
updateCalls int
33+
}
34+
35+
type statusCountingClient struct {
36+
ctrlclient.Client
37+
counter statusCallCounter
38+
}
39+
40+
func (c *statusCountingClient) Status() ctrlclient.SubResourceWriter {
41+
return &statusCountingWriter{
42+
SubResourceWriter: c.Client.Status(),
43+
counter: &c.counter,
44+
}
45+
}
46+
47+
type statusCountingWriter struct {
48+
ctrlclient.SubResourceWriter
49+
counter *statusCallCounter
50+
}
51+
52+
func (w *statusCountingWriter) Update(ctx context.Context, obj ctrlclient.Object, opts ...ctrlclient.SubResourceUpdateOption) error {
53+
w.counter.updateCalls++
54+
return w.SubResourceWriter.Update(ctx, obj, opts...)
55+
}
56+
57+
func (w *statusCountingWriter) Patch(ctx context.Context, obj ctrlclient.Object, patch ctrlclient.Patch, opts ...ctrlclient.SubResourcePatchOption) error {
58+
w.counter.patchCalls++
59+
return w.SubResourceWriter.Patch(ctx, obj, patch, opts...)
60+
}
61+
62+
func TestPatchDeploymentStatusUsesStatusPatch(t *testing.T) {
63+
scheme := runtime.NewScheme()
64+
if err := apps.AddToScheme(scheme); err != nil {
65+
t.Fatalf("add apps/v1 scheme failed: %v", err)
66+
}
67+
68+
baseDeployment := &apps.Deployment{
69+
ObjectMeta: metav1.ObjectMeta{
70+
Name: "demo",
71+
Namespace: "default",
72+
Generation: 5,
73+
},
74+
Status: apps.DeploymentStatus{
75+
ObservedGeneration: 1,
76+
},
77+
}
78+
79+
baseClient := ctrlfake.NewClientBuilder().
80+
WithScheme(scheme).
81+
WithStatusSubresource(&apps.Deployment{}).
82+
WithObjects(baseDeployment).
83+
Build()
84+
countingClient := &statusCountingClient{Client: baseClient}
85+
dc := &DeploymentController{runtimeClient: countingClient}
86+
87+
current := &apps.Deployment{}
88+
key := ctrlclient.ObjectKeyFromObject(baseDeployment)
89+
if err := countingClient.Get(context.TODO(), key, current); err != nil {
90+
t.Fatalf("get deployment failed: %v", err)
91+
}
92+
oldD := current.DeepCopy()
93+
newD := current.DeepCopy()
94+
newD.Status.ObservedGeneration = newD.Generation
95+
96+
if err := dc.patchDeploymentStatus(context.TODO(), oldD, newD); err != nil {
97+
t.Fatalf("patch deployment status failed: %v", err)
98+
}
99+
if countingClient.counter.patchCalls != 1 {
100+
t.Fatalf("expected 1 status patch call, got %d", countingClient.counter.patchCalls)
101+
}
102+
if countingClient.counter.updateCalls != 0 {
103+
t.Fatalf("expected 0 status update calls, got %d", countingClient.counter.updateCalls)
104+
}
105+
106+
result := &apps.Deployment{}
107+
if err := countingClient.Get(context.TODO(), key, result); err != nil {
108+
t.Fatalf("get patched deployment failed: %v", err)
109+
}
110+
if result.Status.ObservedGeneration != result.Generation {
111+
t.Fatalf("expected observedGeneration=%d, got %d", result.Generation, result.Status.ObservedGeneration)
112+
}
113+
}

pkg/controller/deployment/sync.go

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ func (dc *DeploymentController) getNewReplicaSet(ctx context.Context, d *apps.De
133133
}
134134
return rsCopy, nil
135135
}
136+
oldD := d.DeepCopy()
136137

137138
// Should use the revision in existingNewRS's annotation, since it set by before
138139
needsUpdate := deploymentutil.SetDeploymentRevision(d, rsCopy.Annotations[deploymentutil.RevisionAnnotation])
@@ -148,8 +149,9 @@ func (dc *DeploymentController) getNewReplicaSet(ctx context.Context, d *apps.De
148149
}
149150

150151
if needsUpdate {
151-
var err error
152-
if err = dc.runtimeClient.Status().Update(ctx, d); err != nil {
152+
// Patch instead of Update: only send the status delta so unknown
153+
// fields from newer API servers are not erased (version-skew safety).
154+
if err := dc.runtimeClient.Status().Patch(ctx, d, client.MergeFrom(oldD)); err != nil {
153155
return nil, err
154156
}
155157
}
@@ -234,15 +236,17 @@ func (dc *DeploymentController) getNewReplicaSet(ctx context.Context, d *apps.De
234236
if d.Status.CollisionCount == nil {
235237
d.Status.CollisionCount = new(int32)
236238
}
239+
oldD := d.DeepCopy()
240+
237241
preCollisionCount := *d.Status.CollisionCount
238242
*d.Status.CollisionCount++
239-
// Update the collisionCount for the Deployment and let it requeue by returning the original
240-
// error.
241-
dErr := dc.runtimeClient.Status().Update(ctx, d)
243+
// Patch instead of Update: send only the collisionCount delta.
244+
dBase := oldD.DeepCopy()
245+
dErr := dc.runtimeClient.Status().Patch(ctx, d, client.MergeFrom(dBase))
242246
if dErr == nil {
243247
klog.V(2).Infof("Found a hash collision for deployment %q - bumping collisionCount (%d->%d) to resolve it", d.Name, preCollisionCount, *d.Status.CollisionCount)
244248
} else {
245-
klog.Errorf("Failed to update deployment collision count: %v", dErr)
249+
klog.Errorf("Failed to patch deployment collision count: %v", dErr)
246250
}
247251
return nil, err
248252
case errors.HasStatusCause(err, v1.NamespaceTerminatingCause):
@@ -251,13 +255,15 @@ func (dc *DeploymentController) getNewReplicaSet(ctx context.Context, d *apps.De
251255
case err != nil:
252256
msg := fmt.Sprintf("Failed to create new replica set %q: %v", newRS.Name, err)
253257
if deploymentutil.HasProgressDeadline(d) {
258+
oldD := d.DeepCopy()
254259
cond := deploymentutil.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionFalse, deploymentutil.FailedRSCreateReason, msg)
260+
// Patch instead of Update: only the new condition is sent
255261
deploymentutil.SetDeploymentCondition(&d.Status, *cond)
256262
// We don't really care about this error at this point, since we have a bigger issue to report.
257263
// TODO: Identify which errors are permanent and switch DeploymentIsFailed to take into account
258264
// these reasons as well. Related issue: https://github.com/kubernetes/kubernetes/issues/18568
259-
if updateErr := dc.runtimeClient.Status().Update(ctx, d); updateErr != nil {
260-
klog.Errorf("Failed to update deployment status after RS creation failure: %v", updateErr)
265+
if updateErr := dc.runtimeClient.Status().Patch(ctx, d, client.MergeFrom(oldD)); updateErr != nil {
266+
klog.Errorf("Failed to patch deployment status after RS creation failure: %v", updateErr)
261267
}
262268
}
263269
dc.eventRecorder.Eventf(d, v1.EventTypeWarning, deploymentutil.FailedRSCreateReason, msg)
@@ -266,7 +272,7 @@ func (dc *DeploymentController) getNewReplicaSet(ctx context.Context, d *apps.De
266272
if !alreadyExists && newReplicasCount > 0 {
267273
dc.eventRecorder.Eventf(d, v1.EventTypeNormal, "ScalingReplicaSet", "Scaled up replica set %s to %d", createdRS.Name, newReplicasCount)
268274
}
269-
275+
oldD := d.DeepCopy()
270276
needsUpdate := deploymentutil.SetDeploymentRevision(d, newRevision)
271277
if !alreadyExists && deploymentutil.HasProgressDeadline(d) {
272278
msg := fmt.Sprintf("Created new replica set %q", createdRS.Name)
@@ -275,8 +281,9 @@ func (dc *DeploymentController) getNewReplicaSet(ctx context.Context, d *apps.De
275281
needsUpdate = true
276282
}
277283
if needsUpdate {
278-
if updateErr := dc.runtimeClient.Status().Update(ctx, d); updateErr != nil {
279-
klog.Errorf("Failed to update deployment status: %v", updateErr)
284+
// Patch instead of Update: only the revision / Progressing condition delta is sent.
285+
if updateErr := dc.runtimeClient.Status().Patch(ctx, d, client.MergeFrom(oldD)); updateErr != nil {
286+
klog.Errorf("Failed to patch deployment status: %v", updateErr)
280287
err = updateErr
281288
}
282289
}

pkg/util/workloads_utils.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,21 +206,30 @@ func UpdateFinalizer(c client.Client, object client.Object, op FinalizerOpType,
206206
return getErr
207207
}
208208
finalizers := fetchedObject.GetFinalizers()
209+
209210
switch op {
210211
case AddFinalizerOpType:
211212
if controllerutil.ContainsFinalizer(fetchedObject, finalizer) {
212213
return nil
213214
}
214215
finalizers = append(finalizers, finalizer)
216+
215217
case RemoveFinalizerOpType:
216218
finalizerSet := sets.NewString(finalizers...)
217219
if !finalizerSet.Has(finalizer) {
218220
return nil
219221
}
220222
finalizers = finalizerSet.Delete(finalizer).List()
221223
}
224+
225+
// Take a snapshot before mutating so only the finalizer delta is sent.
226+
// Using Patch instead of Update prevents unknown/new API fields from being
227+
// erased when Rollout is built against an older k8s version than the cluster.
228+
baseObject := fetchedObject.DeepCopyObject().(client.Object)
229+
222230
fetchedObject.SetFinalizers(finalizers)
223-
return c.Update(context.TODO(), fetchedObject)
231+
patch := client.MergeFromWithOptions(baseObject, client.MergeFromWithOptimisticLock{})
232+
return c.Patch(context.TODO(), fetchedObject, patch)
224233
})
225234
}
226235

0 commit comments

Comments
 (0)