diff --git a/pkg/sync/common/types.go b/pkg/sync/common/types.go index e9613b7c8..bcff45b7a 100644 --- a/pkg/sync/common/types.go +++ b/pkg/sync/common/types.go @@ -27,6 +27,8 @@ const ( SyncOptionPruneLast = "PruneLast=true" // Sync option that enables use of replace or create command instead of apply SyncOptionReplace = "Replace=true" + // Sync option that enables use of --server-side flag instead of client-side + SyncOptionServerSideApply = "ServerSideApply=true" ) type PermissionValidator func(un *unstructured.Unstructured, res *metav1.APIResource) error diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index a2692cfa9..60051e9f3 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -179,6 +179,12 @@ func WithReplace(replace bool) SyncOpt { } } +func WithServerSideApply(serverSideApply bool) SyncOpt { + return func(ctx *syncContext) { + ctx.serverSideApply = serverSideApply + } +} + // NewSyncContext creates new instance of a SyncContext func NewSyncContext( revision string, @@ -320,6 +326,7 @@ type syncContext struct { resourcesFilter func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool prune bool replace bool + serverSideApply bool pruneLast bool prunePropagationPolicy *metav1.DeletionPropagation @@ -847,7 +854,7 @@ func (sc *syncContext) ensureCRDReady(name string) { }) } -func (sc *syncContext) applyObject(t *syncTask, dryRun bool, force bool, validate bool) (common.ResultCode, string) { +func (sc *syncContext) applyObject(t *syncTask, dryRun, force, validate bool) (common.ResultCode, string) { dryRunStrategy := cmdutil.DryRunNone if dryRun { dryRunStrategy = cmdutil.DryRunClient @@ -856,6 +863,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun bool, force bool, validat var err error var message string shouldReplace := sc.replace || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionReplace) + serverSideApply := sc.serverSideApply || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply) if shouldReplace { if t.liveObj != nil { // Avoid using `kubectl replace` for CRDs since 'replace' might recreate resource and so delete all CRD instances @@ -875,7 +883,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun bool, force bool, validat message, err = sc.resourceOps.CreateResource(context.TODO(), t.targetObj, dryRunStrategy, validate) } } else { - message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate) + message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply) } if err != nil { return common.ResultCodeSyncFailed, err.Error() diff --git a/pkg/sync/sync_context_test.go b/pkg/sync/sync_context_test.go index 2cd6affc4..782bed3de 100644 --- a/pkg/sync/sync_context_test.go +++ b/pkg/sync/sync_context_test.go @@ -565,6 +565,52 @@ func TestSync_Replace(t *testing.T) { } } +func withServerSideApplyAnnotation(un *unstructured.Unstructured) *unstructured.Unstructured { + un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: synccommon.SyncOptionServerSideApply}) + return un +} + +func withReplaceAndServerSideApplyAnnotations(un *unstructured.Unstructured) *unstructured.Unstructured { + un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Replace=true,ServerSideApply=true"}) + return un +} + +func TestSync_ServerSideApply(t *testing.T) { + testCases := []struct { + name string + target *unstructured.Unstructured + live *unstructured.Unstructured + commandUsed string + serverSideApply bool + }{ + {"NoAnnotation", NewPod(), NewPod(), "apply", false}, + {"ServerSideApplyAnnotationIsSet", withServerSideApplyAnnotation(NewPod()), NewPod(), "apply", true}, + {"ServerSideApplyAndReplaceAnnotationsAreSet", withReplaceAndServerSideApplyAnnotations(NewPod()), NewPod(), "replace", false}, + {"LiveObjectMissing", withReplaceAnnotation(NewPod()), nil, "create", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + syncCtx := newTestSyncCtx() + + tc.target.SetNamespace(FakeArgoCDNamespace) + if tc.live != nil { + tc.live.SetNamespace(FakeArgoCDNamespace) + } + syncCtx.resources = groupResources(ReconciliationResult{ + Live: []*unstructured.Unstructured{tc.live}, + Target: []*unstructured.Unstructured{tc.target}, + }) + + syncCtx.Sync() + + kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd) + assert.Equal(t, tc.commandUsed, kubectl.GetLastResourceCommand(kube.GetResourceKey(tc.target))) + assert.Equal(t, tc.serverSideApply, kubectl.GetLastServerSideApply()) + }) + } +} + func TestSelectiveSyncOnly(t *testing.T) { pod1 := NewPod() pod1.SetName("pod-1") diff --git a/pkg/utils/kube/kubetest/mock.go b/pkg/utils/kube/kubetest/mock.go index db588deba..e3b80e898 100644 --- a/pkg/utils/kube/kubetest/mock.go +++ b/pkg/utils/kube/kubetest/mock.go @@ -31,6 +31,7 @@ type MockKubectlCmd struct { lastCommandPerResource map[kube.ResourceKey]string lastValidate bool + serverSideApply bool recordLock sync.RWMutex } @@ -65,6 +66,19 @@ func (k *MockKubectlCmd) GetLastValidate() bool { return validate } +func (k *MockKubectlCmd) SetLastServerSideApply(serverSideApply bool) { + k.recordLock.Lock() + k.serverSideApply = serverSideApply + k.recordLock.Unlock() +} + +func (k *MockKubectlCmd) GetLastServerSideApply() bool { + k.recordLock.RLock() + serverSideApply := k.serverSideApply + k.recordLock.RUnlock() + return serverSideApply +} + func (k *MockKubectlCmd) NewDynamicClient(config *rest.Config) (dynamic.Interface, error) { return k.DynamicClient, nil } @@ -107,8 +121,9 @@ func (k *MockKubectlCmd) UpdateResource(ctx context.Context, obj *unstructured.U return obj, command.Err } -func (k *MockKubectlCmd) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) { +func (k *MockKubectlCmd) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool) (string, error) { k.SetLastValidate(validate) + k.SetLastServerSideApply(serverSideApply) k.SetLastResourceCommand(kube.GetResourceKey(obj), "apply") command, ok := k.Commands[obj.GetName()] if !ok { diff --git a/pkg/utils/kube/resource_ops.go b/pkg/utils/kube/resource_ops.go index b77082cd7..c110bd0e9 100644 --- a/pkg/utils/kube/resource_ops.go +++ b/pkg/utils/kube/resource_ops.go @@ -36,7 +36,7 @@ import ( // ResourceOperations provides methods to manage k8s resources type ResourceOperations interface { - ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) + ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool) (string, error) ReplaceResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force bool) (string, error) CreateResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, validate bool) (string, error) UpdateResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy) (*unstructured.Unstructured, error) @@ -220,7 +220,7 @@ func (k *kubectlResourceOperations) UpdateResource(ctx context.Context, obj *uns } // ApplyResource performs an apply of a unstructured resource -func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) { +func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool) (string, error) { span := k.tracer.StartSpan("ApplyResource") span.SetBaggageItem("kind", obj.GetKind()) span.SetBaggageItem("name", obj.GetName()) @@ -233,7 +233,7 @@ func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unst } defer cleanup() - applyOpts, err := k.newApplyOptions(ioStreams, obj, fileName, validate, force, dryRunStrategy) + applyOpts, err := k.newApplyOptions(ioStreams, obj, fileName, validate, force, serverSideApply, dryRunStrategy) if err != nil { return err } @@ -241,7 +241,7 @@ func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unst }) } -func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.IOStreams, obj *unstructured.Unstructured, fileName string, validate bool, force bool, dryRunStrategy cmdutil.DryRunStrategy) (*apply.ApplyOptions, error) { +func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.IOStreams, obj *unstructured.Unstructured, fileName string, validate bool, force, serverSideApply bool, dryRunStrategy cmdutil.DryRunStrategy) (*apply.ApplyOptions, error) { o := apply.NewApplyOptions(ioStreams) dynamicClient, err := dynamic.NewForConfig(k.config) if err != nil { @@ -287,6 +287,7 @@ func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions. o.DeleteOptions.FilenameOptions.Filenames = []string{fileName} o.Namespace = obj.GetNamespace() o.DeleteOptions.ForceDeletion = force + o.ServerSideApply = serverSideApply o.DryRunStrategy = dryRunStrategy return o, nil }