Skip to content
This repository was archived by the owner on Oct 6, 2025. It is now read-only.

Commit b3a2ec1

Browse files
authored
fix(sync): ApplyOutOfSyncOnly=true sync option is not honoured for cluster scoped resources (#765)
* Using live obj to get the resource key if not nil Signed-off-by: Anand Francis Joseph <[email protected]> * Fixed failing unit tests Signed-off-by: Anand Francis Joseph <[email protected]> * Added test case for validating cluster scoped resources with ApplyOutOfSyncOnly=true Signed-off-by: Anand Francis Joseph <[email protected]> * Fixed gofumpt formatting errors Signed-off-by: Anand Francis Joseph <[email protected]> * Corrected unit tests for cluster scoped resources Signed-off-by: Anand Francis Joseph <[email protected]> * Removed unwanted code comments Signed-off-by: Anand Francis Joseph <[email protected]> * Added comments for explaining the reason why ns is set from live object Signed-off-by: Anand Francis Joseph <[email protected]> * Added comments in the unit test Signed-off-by: Anand Francis Joseph <[email protected]> --------- Signed-off-by: Anand Francis Joseph <[email protected]>
1 parent e0a0d5b commit b3a2ec1

File tree

2 files changed

+105
-1
lines changed

2 files changed

+105
-1
lines changed

pkg/sync/sync_context_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,10 +393,13 @@ func TestSyncCreateFailure(t *testing.T) {
393393
func TestSync_ApplyOutOfSyncOnly(t *testing.T) {
394394
pod1 := testingutils.NewPod()
395395
pod1.SetName("pod-1")
396+
pod1.SetNamespace("fake-argocd-ns")
396397
pod2 := testingutils.NewPod()
397398
pod2.SetName("pod-2")
399+
pod2.SetNamespace("fake-argocd-ns")
398400
pod3 := testingutils.NewPod()
399401
pod3.SetName("pod-3")
402+
pod3.SetNamespace("fake-argocd-ns")
400403

401404
syncCtx := newTestSyncCtx(nil)
402405
syncCtx.applyOutOfSyncOnly = true
@@ -506,6 +509,70 @@ func TestSync_ApplyOutOfSyncOnly(t *testing.T) {
506509
})
507510
}
508511

512+
func TestSync_ApplyOutOfSyncOnly_ClusterResources(t *testing.T) {
513+
ns1 := testingutils.NewNamespace()
514+
ns1.SetName("ns-1")
515+
ns1.SetNamespace("")
516+
517+
ns2 := testingutils.NewNamespace()
518+
ns2.SetName("ns-2")
519+
ns1.SetNamespace("")
520+
521+
ns3 := testingutils.NewNamespace()
522+
ns3.SetName("ns-3")
523+
ns3.SetNamespace("")
524+
525+
ns2Target := testingutils.NewNamespace()
526+
ns2Target.SetName("ns-2")
527+
// set namespace for a cluster scoped resource. This is to simulate the behaviour, where the Application's
528+
// spec.destination.namespace is set for all resources that does not have a namespace set, irrespective of whether
529+
// the resource is cluster scoped or namespace scoped.
530+
//
531+
// Refer to https://github.com/argoproj/gitops-engine/blob/8007df5f6c5dd78a1a8cef73569468ce4d83682c/pkg/sync/sync_context.go#L827-L833
532+
ns2Target.SetNamespace("ns-2")
533+
534+
syncCtx := newTestSyncCtx(nil, WithResourceModificationChecker(true, diffResultListClusterResource()))
535+
syncCtx.applyOutOfSyncOnly = true
536+
fakeDisco := syncCtx.disco.(*fakedisco.FakeDiscovery)
537+
fakeDisco.Resources = []*metav1.APIResourceList{
538+
{
539+
GroupVersion: "v1",
540+
APIResources: []metav1.APIResource{
541+
{Kind: "Namespace", Group: "", Version: "v1", Namespaced: false, Verbs: standardVerbs},
542+
},
543+
},
544+
}
545+
546+
t.Run("cluster resource with target ns having namespace filled", func(t *testing.T) {
547+
syncCtx.resources = groupResources(ReconciliationResult{
548+
Live: []*unstructured.Unstructured{nil, ns2, ns3},
549+
Target: []*unstructured.Unstructured{ns1, ns2Target, ns3},
550+
})
551+
552+
syncCtx.Sync()
553+
phase, _, resources := syncCtx.GetState()
554+
assert.Equal(t, synccommon.OperationSucceeded, phase)
555+
assert.Len(t, resources, 1)
556+
for _, r := range resources {
557+
switch r.ResourceKey.Name {
558+
case "ns-1":
559+
// ns-1 namespace does not exist yet in the cluster, so it must create it and resource must go to
560+
// synced state.
561+
assert.Equal(t, synccommon.ResultCodeSynced, r.Status)
562+
case "ns-2":
563+
// ns-2 namespace already exist and is synced. However, the target resource has metadata.namespace set for
564+
// a cluster resource. This namespace must not be synced again, as the object already exists and
565+
// a change in namespace for a cluster resource has no meaning and hence must not be treated as an
566+
// out-of-sync resource.
567+
t.Error("ns-2 should have been skipped, as no change")
568+
case "ns-3":
569+
// ns-3 namespace exists and there is no change in the target's metadata.namespace value. So it must not try to sync again.
570+
t.Error("ns-3 should have been skipped, as no change")
571+
}
572+
}
573+
})
574+
}
575+
509576
func TestSyncPruneFailure(t *testing.T) {
510577
syncCtx := newTestSyncCtx(nil, WithOperationSettings(false, true, false, false))
511578
mockKubectl := &kubetest.MockKubectlCmd{
@@ -2357,3 +2424,28 @@ func TestNeedsClientSideApplyMigration(t *testing.T) {
23572424
})
23582425
}
23592426
}
2427+
2428+
func diffResultListClusterResource() *diff.DiffResultList {
2429+
ns1 := testingutils.NewNamespace()
2430+
ns1.SetName("ns-1")
2431+
ns2 := testingutils.NewNamespace()
2432+
ns2.SetName("ns-2")
2433+
ns3 := testingutils.NewNamespace()
2434+
ns3.SetName("ns-3")
2435+
2436+
diffResultList := diff.DiffResultList{
2437+
Modified: true,
2438+
Diffs: []diff.DiffResult{},
2439+
}
2440+
2441+
nsBytes, _ := json.Marshal(ns1)
2442+
diffResultList.Diffs = append(diffResultList.Diffs, diff.DiffResult{NormalizedLive: nsBytes, PredictedLive: nsBytes, Modified: false})
2443+
2444+
nsBytes, _ = json.Marshal(ns2)
2445+
diffResultList.Diffs = append(diffResultList.Diffs, diff.DiffResult{NormalizedLive: nsBytes, PredictedLive: nsBytes, Modified: false})
2446+
2447+
nsBytes, _ = json.Marshal(ns3)
2448+
diffResultList.Diffs = append(diffResultList.Diffs, diff.DiffResult{NormalizedLive: nsBytes, PredictedLive: nsBytes, Modified: false})
2449+
2450+
return &diffResultList
2451+
}

pkg/sync/sync_task.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,5 +148,17 @@ func (t *syncTask) deleteOnPhaseFailed() bool {
148148
}
149149

150150
func (t *syncTask) resourceKey() kube.ResourceKey {
151-
return kube.GetResourceKey(t.obj())
151+
resourceKey := kube.GetResourceKey(t.obj())
152+
if t.liveObj != nil {
153+
// t.targetObj has a namespace set for cluster-scoped resources, which causes kube.GetResourceKey()
154+
// to produce an incorrect key with the namespace included. For example:
155+
// rbac.authorization.k8s.io/ClusterRole/my-namespace/my-cluster-role
156+
// instead of the correct rbac.authorization.k8s.io/ClusterRole//my-cluster-role.
157+
// To prevent resource lookup issues, we always rely on the namespace of the live object if it is available.
158+
// This logic will work for both cluster scoped and namespace scoped resources.
159+
//
160+
// Refer to https://github.com/argoproj/gitops-engine/blob/8007df5f6c5dd78a1a8cef73569468ce4d83682c/pkg/sync/sync_context.go#L827-L833
161+
resourceKey.Namespace = t.liveObj.GetNamespace()
162+
}
163+
return resourceKey
152164
}

0 commit comments

Comments
 (0)