Skip to content

Commit 89c110b

Browse files
authored
fix: Server-Side diff removed fields missing in diff (#722)
* fix: Server-Side diff removed fields missing in diff Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * add unit test to cover deleted field Signed-off-by: Peter Jiang <peterjiang823@gmail.com> --------- Signed-off-by: Peter Jiang <peterjiang823@gmail.com>
1 parent 90b69e9 commit 89c110b

File tree

6 files changed

+184
-0
lines changed

6 files changed

+184
-0
lines changed

pkg/diff/diff.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,12 +248,28 @@ func removeWebhookMutation(predictedLive, live *unstructured.Unstructured, gvkPa
248248
// Remove fields from predicted live that are not managed by the provided manager
249249
nonArgoFieldsSet := predictedLiveFieldSet.Difference(managerFieldsSet)
250250

251+
// Compare the predicted live with the live resource
252+
comparison, err := typedLive.Compare(typedPredictedLive)
253+
if err != nil {
254+
return nil, fmt.Errorf("error comparing predicted resource to live resource: %w", err)
255+
}
256+
257+
if comparison.Removed != nil && !comparison.Removed.Empty() {
258+
// exclude the removed fields not owned by this manager from the comparison
259+
comparison.Removed = comparison.Removed.Difference(nonArgoFieldsSet)
260+
}
261+
251262
// In case any of the removed fields cause schema violations, we will keep those fields
252263
nonArgoFieldsSet = safelyRemoveFieldsSet(typedPredictedLive, nonArgoFieldsSet)
253264
typedPredictedLive = typedPredictedLive.RemoveItems(nonArgoFieldsSet)
254265

255266
// Apply the predicted live state to the live state to get a diff without mutation webhook fields
256267
typedPredictedLive, err = typedLive.Merge(typedPredictedLive)
268+
269+
// After applying the predicted live to live state, this would cause any removed fields to be restored.
270+
// We need to re-remove these from predicted live.
271+
typedPredictedLive = typedPredictedLive.RemoveItems(comparison.Removed)
272+
257273
if err != nil {
258274
return nil, fmt.Errorf("error applying predicted live to live state: %w", err)
259275
}

pkg/diff/diff_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,6 +1030,31 @@ func TestServerSideDiff(t *testing.T) {
10301030
assert.Empty(t, predictedDeploy.Annotations[AnnotationLastAppliedConfig])
10311031
assert.Empty(t, liveDeploy.Annotations[AnnotationLastAppliedConfig])
10321032
})
1033+
1034+
t.Run("will reflect deletion of labels in predicted live", func(t *testing.T) {
1035+
// given
1036+
t.Parallel()
1037+
liveState := StrToUnstructured(testdata.ServiceLiveLabelYAMLSSD)
1038+
desiredState := StrToUnstructured(testdata.ServiceConfigNoLabelYAMLSSD)
1039+
opts := buildOpts(testdata.ServicePredictedLiveNoLabelJSONSSD)
1040+
1041+
// when
1042+
result, err := serverSideDiff(desiredState, liveState, opts...)
1043+
1044+
// then
1045+
require.NoError(t, err)
1046+
assert.NotNil(t, result)
1047+
assert.True(t, result.Modified)
1048+
1049+
predictedSvc := YamlToSvc(t, result.PredictedLive)
1050+
liveSvc := YamlToSvc(t, result.NormalizedLive)
1051+
1052+
// Ensure that the deleted label is not present in predicted and exists in live
1053+
_, predictedLabelExists := predictedSvc.Labels["delete-me"]
1054+
_, liveLabelExists := liveSvc.Labels["delete-me"]
1055+
assert.False(t, predictedLabelExists)
1056+
assert.True(t, liveLabelExists)
1057+
})
10331058
}
10341059

10351060
func createSecret(data map[string]string) *unstructured.Unstructured {

pkg/diff/testdata/data.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,13 @@ var (
6868

6969
//go:embed ssd-deploy-with-manual-apply-predicted-live.json
7070
DeploymentApplyPredictedLiveJSONSSD string
71+
72+
//go:embed ssd-svc-label-live.yaml
73+
ServiceLiveLabelYAMLSSD string
74+
75+
//go:embed ssd-svc-no-label-config.yaml
76+
ServiceConfigNoLabelYAMLSSD string
77+
78+
//go:embed ssd-svc-no-label-predicted-live.json
79+
ServicePredictedLiveNoLabelJSONSSD string
7180
)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
creationTimestamp: "2025-05-16T19:01:22Z"
5+
labels:
6+
app.kubernetes.io/instance: httpbin
7+
delete-me: delete-value
8+
managedFields:
9+
- apiVersion: v1
10+
fieldsType: FieldsV1
11+
fieldsV1:
12+
f:metadata:
13+
f:labels:
14+
f:app.kubernetes.io/instance: {}
15+
f:delete-me: {}
16+
f:spec:
17+
f:ports:
18+
k:{"port":7777,"protocol":"TCP"}:
19+
.: {}
20+
f:name: {}
21+
f:port: {}
22+
f:protocol: {}
23+
f:targetPort: {}
24+
f:selector: {}
25+
manager: argocd-controller
26+
operation: Apply
27+
time: "2025-05-16T19:01:22Z"
28+
name: httpbin-svc
29+
namespace: httpbin
30+
resourceVersion: "159005"
31+
uid: 61a7a0c2-d973-4333-bbd6-c06ba1c00190
32+
spec:
33+
clusterIP: 10.96.59.144
34+
clusterIPs:
35+
- 10.96.59.144
36+
internalTrafficPolicy: Cluster
37+
ipFamilies:
38+
- IPv4
39+
ipFamilyPolicy: SingleStack
40+
ports:
41+
- name: http-port
42+
port: 7777
43+
protocol: TCP
44+
targetPort: 80
45+
selector:
46+
app: httpbin
47+
sessionAffinity: None
48+
type: ClusterIP
49+
status:
50+
loadBalancer: {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
labels:
5+
app.kubernetes.io/instance: httpbin
6+
name: httpbin-svc
7+
namespace: httpbin
8+
spec:
9+
ports:
10+
- name: http-port
11+
port: 7777
12+
protocol: TCP
13+
targetPort: 80
14+
selector:
15+
app: httpbin
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"apiVersion": "v1",
3+
"kind": "Service",
4+
"metadata": {
5+
"creationTimestamp": "2025-05-16T19:01:22Z",
6+
"labels": {
7+
"app.kubernetes.io/instance": "httpbin"
8+
},
9+
"managedFields": [
10+
{
11+
"apiVersion": "v1",
12+
"fieldsType": "FieldsV1",
13+
"fieldsV1": {
14+
"f:metadata": {
15+
"f:labels": {
16+
"f:app.kubernetes.io/instance": {}
17+
}
18+
},
19+
"f:spec": {
20+
"f:ports": {
21+
"k:{\"port\":7777,\"protocol\":\"TCP\"}": {
22+
".": {},
23+
"f:name": {},
24+
"f:port": {},
25+
"f:protocol": {},
26+
"f:targetPort": {}
27+
}
28+
},
29+
"f:selector": {}
30+
}
31+
},
32+
"manager": "argocd-controller",
33+
"operation": "Apply",
34+
"time": "2025-05-16T19:02:57Z"
35+
}
36+
],
37+
"name": "httpbin-svc",
38+
"namespace": "httpbin",
39+
"resourceVersion": "159005",
40+
"uid": "61a7a0c2-d973-4333-bbd6-c06ba1c00190"
41+
},
42+
"spec": {
43+
"clusterIP": "10.96.59.144",
44+
"clusterIPs": [
45+
"10.96.59.144"
46+
],
47+
"internalTrafficPolicy": "Cluster",
48+
"ipFamilies": [
49+
"IPv4"
50+
],
51+
"ipFamilyPolicy": "SingleStack",
52+
"ports": [
53+
{
54+
"name": "http-port",
55+
"port": 7777,
56+
"protocol": "TCP",
57+
"targetPort": 80
58+
}
59+
],
60+
"selector": {
61+
"app": "httpbin"
62+
},
63+
"sessionAffinity": "None",
64+
"type": "ClusterIP"
65+
},
66+
"status": {
67+
"loadBalancer": {}
68+
}
69+
}

0 commit comments

Comments
 (0)