Skip to content

Commit 1fe1455

Browse files
committed
feat: use server side apply for dry runs where possible
Previously dry runs would always use client side apply. This is because server side apply is not compatible with the namespace auto creation feature - since the server side validation fails in this case (as expected). This change uses server side apply for dry runs where ServerSideApply=true option is set, but not in the case where the CreateNamespace=true option is set. An alternative would be to return an error if both auto-create namespace and server side apply options were set for dry runs. However this would be more of a breaking change Signed-off-by: abuck <[email protected]>
1 parent 90b69e9 commit 1fe1455

File tree

3 files changed

+111
-22
lines changed

3 files changed

+111
-22
lines changed

pkg/sync/sync_context.go

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,15 +1042,7 @@ func (sc *syncContext) ensureCRDReady(name string) error {
10421042
})
10431043
}
10441044

1045-
func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstructured, dryRun bool) bool {
1046-
// if it is a dry run, disable server side apply, as the goal is to validate only the
1047-
// yaml correctness of the rendered manifests.
1048-
// running dry-run in server mode breaks the auto create namespace feature
1049-
// https://github.com/argoproj/argo-cd/issues/13874
1050-
if sc.dryRun || dryRun {
1051-
return false
1052-
}
1053-
1045+
func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstructured) bool {
10541046
resourceHasDisableSSAAnnotation := resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionDisableServerSideApply)
10551047
if resourceHasDisableSSAAnnotation {
10561048
return false
@@ -1059,22 +1051,36 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
10591051
return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
10601052
}
10611053

1054+
func (sc *syncContext) getDryRunStrategy(serverSideApply bool, isAutoCreateNamespaceFeatureEnabled bool) cmdutil.DryRunStrategy {
1055+
if serverSideApply {
1056+
// if auto create namespace is not enabled (CreateNamespace option not set),
1057+
// then server apply can be used for dry runs. Users can choose whether to prioritise
1058+
// convenience of namespaces being created automatically with client side only feedback,
1059+
// or manage the namespace themselves and have rich feedback with server side apply.
1060+
// https://github.com/argoproj/argo-cd/issues/13874
1061+
if !isAutoCreateNamespaceFeatureEnabled {
1062+
return cmdutil.DryRunServer
1063+
}
1064+
sc.log.Info("server side apply is not supported for dry run when auto create namespace is enabled. Falling back to client side dry run")
1065+
}
1066+
return cmdutil.DryRunClient
1067+
}
1068+
10621069
func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
1070+
isResourceEligibleForServerSideApply := sc.shouldUseServerSideApply(t.targetObj)
1071+
10631072
dryRunStrategy := cmdutil.DryRunNone
1064-
if dryRun {
1065-
// irrespective of the dry run mode set in the sync context, always run
1066-
// in client dry run mode as the goal is to validate only the
1067-
// yaml correctness of the rendered manifests.
1068-
// running dry-run in server mode breaks the auto create namespace feature
1069-
// https://github.com/argoproj/argo-cd/issues/13874
1070-
dryRunStrategy = cmdutil.DryRunClient
1073+
if sc.dryRun || dryRun {
1074+
dryRunStrategy = sc.getDryRunStrategy(isResourceEligibleForServerSideApply, sc.syncNamespace != nil)
10711075
}
10721076

1077+
serverSideApply := isResourceEligibleForServerSideApply && dryRunStrategy != cmdutil.DryRunClient
1078+
10731079
var err error
10741080
var message string
10751081
shouldReplace := sc.replace || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionReplace)
10761082
force := sc.force || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionForce)
1077-
serverSideApply := sc.shouldUseServerSideApply(t.targetObj, dryRun)
1083+
10781084
if shouldReplace {
10791085
if t.liveObj != nil {
10801086
// Avoid using `kubectl replace` for CRDs since 'replace' might recreate resource and so delete all CRD instances.

pkg/sync/sync_context_test.go

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"k8s.io/client-go/rest"
2828
testcore "k8s.io/client-go/testing"
2929
"k8s.io/klog/v2/textlogger"
30+
cmdutil "k8s.io/kubectl/pkg/cmd/util"
3031

3132
"github.com/argoproj/gitops-engine/pkg/diff"
3233
"github.com/argoproj/gitops-engine/pkg/health"
@@ -860,9 +861,9 @@ func TestSyncContext_ServerSideApplyWithDryRun(t *testing.T) {
860861
objToUse func(*unstructured.Unstructured) *unstructured.Unstructured
861862
}{
862863
{"BothFlagsFalseAnnotated", false, false, true, withServerSideApplyAnnotation},
863-
{"scDryRunTrueAnnotated", true, false, false, withServerSideApplyAnnotation},
864-
{"dryRunTrueAnnotated", false, true, false, withServerSideApplyAnnotation},
865-
{"BothFlagsTrueAnnotated", true, true, false, withServerSideApplyAnnotation},
864+
{"scDryRunTrueAnnotated", true, false, true, withServerSideApplyAnnotation},
865+
{"dryRunTrueAnnotated", false, true, true, withServerSideApplyAnnotation},
866+
{"BothFlagsTrueAnnotated", true, true, true, withServerSideApplyAnnotation},
866867
{"AnnotatedDisabledSSA", false, false, false, withDisableServerSideApplyAnnotation},
867868
}
868869

@@ -873,7 +874,7 @@ func TestSyncContext_ServerSideApplyWithDryRun(t *testing.T) {
873874
targetObj := tc.objToUse(testingutils.NewPod())
874875

875876
// Execute the shouldUseServerSideApply method and assert expectations
876-
serverSideApply := sc.shouldUseServerSideApply(targetObj, tc.dryRun)
877+
serverSideApply := sc.shouldUseServerSideApply(targetObj)
877878
assert.Equal(t, tc.expectedSSA, serverSideApply)
878879
})
879880
}
@@ -2180,3 +2181,70 @@ func BenchmarkSync(b *testing.B) {
21802181
syncCtx.Sync()
21812182
}
21822183
}
2184+
2185+
func TestSyncContext_ApplyObjectDryRun(t *testing.T) {
2186+
testCases := []struct {
2187+
name string
2188+
target *unstructured.Unstructured
2189+
live *unstructured.Unstructured
2190+
dryRun bool
2191+
syncNamespace func(*unstructured.Unstructured, *unstructured.Unstructured) (bool, error)
2192+
serverSideApply bool
2193+
expectedDryRun cmdutil.DryRunStrategy
2194+
}{
2195+
{
2196+
name: "DryRunWithNoAutoCreateNamespace",
2197+
target: withServerSideApplyAnnotation(testingutils.NewPod()),
2198+
live: testingutils.NewPod(),
2199+
dryRun: true,
2200+
syncNamespace: nil,
2201+
serverSideApply: true,
2202+
expectedDryRun: cmdutil.DryRunServer,
2203+
},
2204+
{
2205+
name: "DryRunWithAutoCreateNamespace",
2206+
target: withServerSideApplyAnnotation(testingutils.NewPod()),
2207+
live: testingutils.NewPod(),
2208+
dryRun: true,
2209+
syncNamespace: func(*unstructured.Unstructured, *unstructured.Unstructured) (bool, error) { return true, nil },
2210+
serverSideApply: true,
2211+
expectedDryRun: cmdutil.DryRunClient,
2212+
},
2213+
{
2214+
name: "NoDryRun",
2215+
target: withServerSideApplyAnnotation(testingutils.NewPod()),
2216+
live: testingutils.NewPod(),
2217+
dryRun: false,
2218+
syncNamespace: nil,
2219+
serverSideApply: true,
2220+
expectedDryRun: cmdutil.DryRunNone,
2221+
},
2222+
}
2223+
2224+
for _, tc := range testCases {
2225+
tc := tc
2226+
t.Run(tc.name, func(t *testing.T) {
2227+
t.Parallel()
2228+
syncCtx := newTestSyncCtx(nil)
2229+
syncCtx.serverSideApply = tc.serverSideApply
2230+
syncCtx.syncNamespace = tc.syncNamespace
2231+
2232+
tc.target.SetNamespace(testingutils.FakeArgoCDNamespace)
2233+
if tc.live != nil {
2234+
tc.live.SetNamespace(testingutils.FakeArgoCDNamespace)
2235+
}
2236+
2237+
task := &syncTask{
2238+
targetObj: tc.target,
2239+
liveObj: tc.live,
2240+
}
2241+
2242+
result, _ := syncCtx.applyObject(task, tc.dryRun, true)
2243+
2244+
assert.Equal(t, synccommon.ResultCodeSynced, result)
2245+
2246+
resourceOps, _ := syncCtx.resourceOps.(*kubetest.MockResourceOps)
2247+
assert.Equal(t, tc.expectedDryRun, resourceOps.GetLastDryRunStrategy())
2248+
})
2249+
}
2250+
}

pkg/utils/kube/kubetest/mock_resource_operations.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type MockResourceOps struct {
2424
serverSideApply bool
2525
serverSideApplyManager string
2626
lastForce bool
27+
lastDryRunStrategy cmdutil.DryRunStrategy
2728

2829
recordLock sync.RWMutex
2930

@@ -106,12 +107,26 @@ func (r *MockResourceOps) GetLastResourceCommand(key kube.ResourceKey) string {
106107
return r.lastCommandPerResource[key]
107108
}
108109

109-
func (r *MockResourceOps) ApplyResource(_ context.Context, obj *unstructured.Unstructured, _ cmdutil.DryRunStrategy, force bool, validate bool, serverSideApply bool, manager string) (string, error) {
110+
func (r *MockResourceOps) SetLastDryRunStrategy(dryRunStrategy cmdutil.DryRunStrategy) {
111+
r.recordLock.Lock()
112+
r.lastDryRunStrategy = dryRunStrategy
113+
r.recordLock.Unlock()
114+
}
115+
116+
func (r *MockResourceOps) GetLastDryRunStrategy() cmdutil.DryRunStrategy {
117+
r.recordLock.RLock()
118+
dryRunStrategy := r.lastDryRunStrategy
119+
r.recordLock.RUnlock()
120+
return dryRunStrategy
121+
}
122+
123+
func (r *MockResourceOps) ApplyResource(_ context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force bool, validate bool, serverSideApply bool, manager string) (string, error) {
110124
r.SetLastValidate(validate)
111125
r.SetLastServerSideApply(serverSideApply)
112126
r.SetLastServerSideApplyManager(manager)
113127
r.SetLastForce(force)
114128
r.SetLastResourceCommand(kube.GetResourceKey(obj), "apply")
129+
r.SetLastDryRunStrategy(dryRunStrategy)
115130
command, ok := r.Commands[obj.GetName()]
116131
if !ok {
117132
return "", nil

0 commit comments

Comments
 (0)