Skip to content

Commit c4c2799

Browse files
committed
fix: ensure status updates are not skipped due to stale cache
Signed-off-by: 浩韵 <[email protected]>
1 parent 310dcff commit c4c2799

File tree

4 files changed

+89
-60
lines changed

4 files changed

+89
-60
lines changed

pkg/controllers/multiclusterservice/endpointslice_dispatch_controller_test.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,7 @@ func TestUpdateEndpointSliceDispatched(t *testing.T) {
7777

7878
// Expectations Setup
7979
mockClient.On("Status").Return(mockStatusWriter)
80-
mockClient.On("Get", mock.Anything, mock.AnythingOfType("types.NamespacedName"), mock.AnythingOfType("*v1alpha1.MultiClusterService"), mock.Anything).
81-
Run(func(args mock.Arguments) {
82-
arg := args.Get(2).(*networkingv1alpha1.MultiClusterService)
83-
*arg = *tt.mcs // Copy the input MCS to the output
84-
}).Return(nil)
85-
86-
mockStatusWriter.On("Update", mock.Anything, mock.AnythingOfType("*v1alpha1.MultiClusterService"), mock.Anything).
80+
mockStatusWriter.On("Patch", mock.Anything, mock.AnythingOfType("*v1alpha1.MultiClusterService"), mock.Anything, mock.Anything).
8781
Run(func(args mock.Arguments) {
8882
mcs := args.Get(1).(*networkingv1alpha1.MultiClusterService)
8983
mcs.Status.Conditions = []metav1.Condition{tt.expectedCondition}

pkg/controllers/remediation/remedy_controller_test.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,11 @@ func setupScheme() *runtime.Scheme {
192192

193193
// Helper function to create a fake client
194194
func createFakeClient(scheme *runtime.Scheme, objs ...runtime.Object) client.Client {
195-
return fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(objs...).Build()
195+
return fake.NewClientBuilder().
196+
WithScheme(scheme).
197+
WithRuntimeObjects(objs...).
198+
WithStatusSubresource(&clusterv1alpha1.Cluster{}).
199+
Build()
196200
}
197201

198202
// Helper function to set up the manager

pkg/util/helper/status.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import (
2020
"context"
2121
"fmt"
2222

23-
"k8s.io/apimachinery/pkg/api/equality"
2423
"sigs.k8s.io/controller-runtime/pkg/client"
2524
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2625
)
@@ -38,20 +37,17 @@ import (
3837
// already exist.
3938
func UpdateStatus(ctx context.Context, c client.Client, obj client.Object, f controllerutil.MutateFn) (controllerutil.OperationResult, error) {
4039
key := client.ObjectKeyFromObject(obj)
41-
if err := c.Get(ctx, key, obj); err != nil {
42-
return controllerutil.OperationResultNone, err
43-
}
4440

45-
existing := obj.DeepCopyObject()
41+
oldObj := obj.DeepCopyObject().(client.Object)
4642
if err := mutate(f, key, obj); err != nil {
4743
return controllerutil.OperationResultNone, err
4844
}
4945

50-
if equality.Semantic.DeepEqual(existing, obj) {
51-
return controllerutil.OperationResultNone, nil
52-
}
53-
54-
if err := c.Status().Update(ctx, obj); err != nil {
46+
// Patch status directly without checking for equality to ensure the status is always updated,
47+
// even if the content appears unchanged. This avoids issues where stale cache data might cause
48+
// updates to be skipped incorrectly.
49+
// FYI: https://github.com/karmada-io/karmada/issues/6858
50+
if err := c.Status().Patch(ctx, obj, client.MergeFrom(oldObj)); err != nil {
5551
return controllerutil.OperationResultNone, err
5652
}
5753
return controllerutil.OperationResultUpdatedStatusOnly, nil

pkg/util/helper/status_test.go

Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,30 @@ import (
2424
"github.com/stretchr/testify/assert"
2525
corev1 "k8s.io/api/core/v1"
2626
apierrors "k8s.io/apimachinery/pkg/api/errors"
27+
"k8s.io/apimachinery/pkg/api/meta"
2728
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2829
"k8s.io/apimachinery/pkg/runtime"
2930
"k8s.io/apimachinery/pkg/types"
3031
"sigs.k8s.io/controller-runtime/pkg/client"
3132
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3233
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
34+
35+
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
3336
)
3437

3538
func TestUpdateStatus(t *testing.T) {
3639
scheme := runtime.NewScheme()
3740
_ = corev1.AddToScheme(scheme)
41+
_ = workv1alpha1.Install(scheme)
3842

3943
tests := []struct {
4044
name string
41-
setupObj *corev1.Pod
42-
mutateStatus func(*corev1.Pod)
45+
setupObj client.Object
46+
mutateStatus func(client.Object)
4347
statusError bool
4448
expectedOp controllerutil.OperationResult
4549
expectedError string
50+
verify func(*testing.T, client.Client)
4651
}{
4752
{
4853
name: "successful status update",
@@ -55,12 +60,18 @@ func TestUpdateStatus(t *testing.T) {
5560
Phase: corev1.PodPending,
5661
},
5762
},
58-
mutateStatus: func(pod *corev1.Pod) {
63+
mutateStatus: func(obj client.Object) {
64+
pod := obj.(*corev1.Pod)
5965
pod.Status.Phase = corev1.PodRunning
6066
},
6167
statusError: false,
6268
expectedOp: controllerutil.OperationResultUpdatedStatusOnly,
6369
expectedError: "",
70+
verify: func(t *testing.T, c client.Client) {
71+
pod := &corev1.Pod{}
72+
assert.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: "test-pod", Namespace: "default"}, pod))
73+
assert.Equal(t, corev1.PodRunning, pod.Status.Phase)
74+
},
6475
},
6576
{
6677
name: "status update error",
@@ -73,15 +84,16 @@ func TestUpdateStatus(t *testing.T) {
7384
Phase: corev1.PodPending,
7485
},
7586
},
76-
mutateStatus: func(pod *corev1.Pod) {
87+
mutateStatus: func(obj client.Object) {
88+
pod := obj.(*corev1.Pod)
7789
pod.Status.Phase = corev1.PodRunning
7890
},
7991
statusError: true,
8092
expectedOp: controllerutil.OperationResultNone,
8193
expectedError: "Internal error occurred: status update failed",
8294
},
8395
{
84-
name: "no changes needed",
96+
name: "no changes, should still update",
8597
setupObj: &corev1.Pod{
8698
ObjectMeta: metav1.ObjectMeta{
8799
Name: "test-pod",
@@ -91,22 +103,54 @@ func TestUpdateStatus(t *testing.T) {
91103
Phase: corev1.PodRunning,
92104
},
93105
},
94-
mutateStatus: func(_ *corev1.Pod) {
95-
// No changes to status
106+
mutateStatus: func(_ client.Object) {
107+
// No changes
96108
},
97109
statusError: false,
98-
expectedOp: controllerutil.OperationResultNone,
110+
expectedOp: controllerutil.OperationResultUpdatedStatusOnly,
99111
expectedError: "",
112+
verify: func(t *testing.T, c client.Client) {
113+
pod := &corev1.Pod{}
114+
assert.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: "test-pod", Namespace: "default"}, pod))
115+
assert.Equal(t, corev1.PodRunning, pod.Status.Phase)
116+
},
100117
},
101118
{
102-
name: "object not found",
103-
setupObj: nil, // Not create the object
104-
mutateStatus: func(pod *corev1.Pod) {
105-
pod.Status.Phase = corev1.PodRunning
119+
name: "update with same WorkApplied condition, should still update",
120+
setupObj: &workv1alpha1.Work{
121+
ObjectMeta: metav1.ObjectMeta{
122+
Name: "test-work",
123+
Namespace: "default",
124+
},
125+
Status: workv1alpha1.WorkStatus{
126+
Conditions: []metav1.Condition{
127+
{
128+
Type: workv1alpha1.WorkApplied,
129+
Status: metav1.ConditionTrue,
130+
Reason: "AppliedSuccessful",
131+
Message: "Manifest has been successfully applied",
132+
},
133+
},
134+
},
135+
},
136+
mutateStatus: func(obj client.Object) {
137+
meta.SetStatusCondition(&obj.(*workv1alpha1.Work).Status.Conditions, metav1.Condition{
138+
Type: workv1alpha1.WorkApplied,
139+
Status: metav1.ConditionTrue,
140+
Reason: "AppliedSuccessful",
141+
Message: "Manifest has been successfully applied",
142+
})
143+
},
144+
expectedOp: controllerutil.OperationResultUpdatedStatusOnly,
145+
verify: func(t *testing.T, c client.Client) {
146+
work := &workv1alpha1.Work{}
147+
assert.NoError(t, c.Get(context.TODO(), types.NamespacedName{Name: "test-work", Namespace: "default"}, work))
148+
149+
cond := meta.FindStatusCondition(work.Status.Conditions, workv1alpha1.WorkApplied)
150+
assert.NotNil(t, cond)
151+
assert.Equal(t, metav1.ConditionTrue, cond.Status)
152+
assert.Equal(t, "AppliedSuccessful", cond.Reason)
106153
},
107-
statusError: false,
108-
expectedOp: controllerutil.OperationResultNone,
109-
expectedError: "not found",
110154
},
111155
{
112156
name: "mutation error",
@@ -116,9 +160,9 @@ func TestUpdateStatus(t *testing.T) {
116160
Namespace: "default",
117161
},
118162
},
119-
mutateStatus: func(pod *corev1.Pod) {
163+
mutateStatus: func(obj client.Object) {
120164
// Simulate mutation error by changing name
121-
pod.Name = "different-name"
165+
obj.SetName("different-name")
122166
},
123167
statusError: false,
124168
expectedOp: controllerutil.OperationResultNone,
@@ -131,31 +175,19 @@ func TestUpdateStatus(t *testing.T) {
131175
clientBuilder := fake.NewClientBuilder().WithScheme(scheme)
132176
if tt.setupObj != nil {
133177
clientBuilder = clientBuilder.WithObjects(tt.setupObj)
134-
}
135-
fakeClient := clientBuilder.Build()
136-
137-
var client client.Client
138-
if tt.statusError {
139-
client = &mockClient{
140-
Client: fakeClient,
141-
shouldError: true,
178+
if _, ok := tt.setupObj.(*workv1alpha1.Work); ok {
179+
clientBuilder = clientBuilder.WithStatusSubresource(tt.setupObj)
142180
}
143-
} else {
144-
client = fakeClient
145181
}
146182

147-
// Create a new object for update
148-
obj := &corev1.Pod{
149-
ObjectMeta: metav1.ObjectMeta{
150-
Name: "test-pod",
151-
Namespace: "default",
152-
},
183+
c := client.Client(clientBuilder.Build())
184+
if tt.statusError {
185+
c = &mockClient{Client: c, shouldError: true}
153186
}
154187

155-
// Run the update
156-
op, err := UpdateStatus(context.TODO(), client, obj, func() error {
188+
op, err := UpdateStatus(context.TODO(), c, tt.setupObj, func() error {
157189
if tt.mutateStatus != nil {
158-
tt.mutateStatus(obj)
190+
tt.mutateStatus(tt.setupObj)
159191
}
160192
return nil
161193
})
@@ -171,12 +203,8 @@ func TestUpdateStatus(t *testing.T) {
171203
// Check operation result
172204
assert.Equal(t, tt.expectedOp, op)
173205

174-
// If successful update, verify the status was actually changed
175-
if tt.expectedOp == controllerutil.OperationResultUpdatedStatusOnly {
176-
updatedPod := &corev1.Pod{}
177-
err := client.Get(context.TODO(), types.NamespacedName{Name: "test-pod", Namespace: "default"}, updatedPod)
178-
assert.NoError(t, err)
179-
assert.Equal(t, corev1.PodRunning, updatedPod.Status.Phase)
206+
if tt.verify != nil {
207+
tt.verify(t, c)
180208
}
181209
})
182210
}
@@ -359,6 +387,13 @@ func (m *mockStatusWriter) Update(_ context.Context, _ client.Object, _ ...clien
359387
return nil
360388
}
361389

390+
func (m *mockStatusWriter) Patch(_ context.Context, _ client.Object, _ client.Patch, _ ...client.SubResourcePatchOption) error {
391+
if m.shouldError {
392+
return apierrors.NewInternalError(fmt.Errorf("status update failed"))
393+
}
394+
return nil
395+
}
396+
362397
// mockClient wraps the fake client and returns our mock status writer
363398
type mockClient struct {
364399
client.Client

0 commit comments

Comments
 (0)