Skip to content

Commit d8b1a12

Browse files
authored
feat: add basic support for server-side apply (argoproj#363)
See argoproj/argo-cd#2267 Signed-off-by: Mathieu Parent <[email protected]>
1 parent a586397 commit d8b1a12

File tree

5 files changed

+79
-7
lines changed

5 files changed

+79
-7
lines changed

pkg/sync/common/types.go

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ const (
2727
SyncOptionPruneLast = "PruneLast=true"
2828
// Sync option that enables use of replace or create command instead of apply
2929
SyncOptionReplace = "Replace=true"
30+
// Sync option that enables use of --server-side flag instead of client-side
31+
SyncOptionServerSideApply = "ServerSideApply=true"
3032
)
3133

3234
type PermissionValidator func(un *unstructured.Unstructured, res *metav1.APIResource) error

pkg/sync/sync_context.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ func WithReplace(replace bool) SyncOpt {
179179
}
180180
}
181181

182+
func WithServerSideApply(serverSideApply bool) SyncOpt {
183+
return func(ctx *syncContext) {
184+
ctx.serverSideApply = serverSideApply
185+
}
186+
}
187+
182188
// NewSyncContext creates new instance of a SyncContext
183189
func NewSyncContext(
184190
revision string,
@@ -320,6 +326,7 @@ type syncContext struct {
320326
resourcesFilter func(key kube.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool
321327
prune bool
322328
replace bool
329+
serverSideApply bool
323330
pruneLast bool
324331
prunePropagationPolicy *metav1.DeletionPropagation
325332

@@ -847,7 +854,7 @@ func (sc *syncContext) ensureCRDReady(name string) error {
847854
})
848855
}
849856

850-
func (sc *syncContext) applyObject(t *syncTask, dryRun bool, force bool, validate bool) (common.ResultCode, string) {
857+
func (sc *syncContext) applyObject(t *syncTask, dryRun, force, validate bool) (common.ResultCode, string) {
851858
dryRunStrategy := cmdutil.DryRunNone
852859
if dryRun {
853860
dryRunStrategy = cmdutil.DryRunClient
@@ -856,6 +863,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun bool, force bool, validat
856863
var err error
857864
var message string
858865
shouldReplace := sc.replace || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionReplace)
866+
serverSideApply := sc.serverSideApply || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
859867
if shouldReplace {
860868
if t.liveObj != nil {
861869
// 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
875883
message, err = sc.resourceOps.CreateResource(context.TODO(), t.targetObj, dryRunStrategy, validate)
876884
}
877885
} else {
878-
message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate)
886+
message, err = sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply)
879887
}
880888
if err != nil {
881889
return common.ResultCodeSyncFailed, err.Error()

pkg/sync/sync_context_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,52 @@ func TestSync_Replace(t *testing.T) {
565565
}
566566
}
567567

568+
func withServerSideApplyAnnotation(un *unstructured.Unstructured) *unstructured.Unstructured {
569+
un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: synccommon.SyncOptionServerSideApply})
570+
return un
571+
}
572+
573+
func withReplaceAndServerSideApplyAnnotations(un *unstructured.Unstructured) *unstructured.Unstructured {
574+
un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Replace=true,ServerSideApply=true"})
575+
return un
576+
}
577+
578+
func TestSync_ServerSideApply(t *testing.T) {
579+
testCases := []struct {
580+
name string
581+
target *unstructured.Unstructured
582+
live *unstructured.Unstructured
583+
commandUsed string
584+
serverSideApply bool
585+
}{
586+
{"NoAnnotation", NewPod(), NewPod(), "apply", false},
587+
{"ServerSideApplyAnnotationIsSet", withServerSideApplyAnnotation(NewPod()), NewPod(), "apply", true},
588+
{"ServerSideApplyAndReplaceAnnotationsAreSet", withReplaceAndServerSideApplyAnnotations(NewPod()), NewPod(), "replace", false},
589+
{"LiveObjectMissing", withReplaceAnnotation(NewPod()), nil, "create", false},
590+
}
591+
592+
for _, tc := range testCases {
593+
t.Run(tc.name, func(t *testing.T) {
594+
syncCtx := newTestSyncCtx()
595+
596+
tc.target.SetNamespace(FakeArgoCDNamespace)
597+
if tc.live != nil {
598+
tc.live.SetNamespace(FakeArgoCDNamespace)
599+
}
600+
syncCtx.resources = groupResources(ReconciliationResult{
601+
Live: []*unstructured.Unstructured{tc.live},
602+
Target: []*unstructured.Unstructured{tc.target},
603+
})
604+
605+
syncCtx.Sync()
606+
607+
kubectl, _ := syncCtx.kubectl.(*kubetest.MockKubectlCmd)
608+
assert.Equal(t, tc.commandUsed, kubectl.GetLastResourceCommand(kube.GetResourceKey(tc.target)))
609+
assert.Equal(t, tc.serverSideApply, kubectl.GetLastServerSideApply())
610+
})
611+
}
612+
}
613+
568614
func TestSelectiveSyncOnly(t *testing.T) {
569615
pod1 := NewPod()
570616
pod1.SetName("pod-1")

pkg/utils/kube/kubetest/mock.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type MockKubectlCmd struct {
3131

3232
lastCommandPerResource map[kube.ResourceKey]string
3333
lastValidate bool
34+
serverSideApply bool
3435
recordLock sync.RWMutex
3536
}
3637

@@ -65,6 +66,19 @@ func (k *MockKubectlCmd) GetLastValidate() bool {
6566
return validate
6667
}
6768

69+
func (k *MockKubectlCmd) SetLastServerSideApply(serverSideApply bool) {
70+
k.recordLock.Lock()
71+
k.serverSideApply = serverSideApply
72+
k.recordLock.Unlock()
73+
}
74+
75+
func (k *MockKubectlCmd) GetLastServerSideApply() bool {
76+
k.recordLock.RLock()
77+
serverSideApply := k.serverSideApply
78+
k.recordLock.RUnlock()
79+
return serverSideApply
80+
}
81+
6882
func (k *MockKubectlCmd) NewDynamicClient(config *rest.Config) (dynamic.Interface, error) {
6983
return k.DynamicClient, nil
7084
}
@@ -107,8 +121,9 @@ func (k *MockKubectlCmd) UpdateResource(ctx context.Context, obj *unstructured.U
107121
return obj, command.Err
108122
}
109123

110-
func (k *MockKubectlCmd) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) {
124+
func (k *MockKubectlCmd) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool) (string, error) {
111125
k.SetLastValidate(validate)
126+
k.SetLastServerSideApply(serverSideApply)
112127
k.SetLastResourceCommand(kube.GetResourceKey(obj), "apply")
113128
command, ok := k.Commands[obj.GetName()]
114129
if !ok {

pkg/utils/kube/resource_ops.go

+5-4
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import (
3838

3939
// ResourceOperations provides methods to manage k8s resources
4040
type ResourceOperations interface {
41-
ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error)
41+
ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool) (string, error)
4242
ReplaceResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force bool) (string, error)
4343
CreateResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, validate bool) (string, error)
4444
UpdateResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy) (*unstructured.Unstructured, error)
@@ -224,7 +224,7 @@ func (k *kubectlResourceOperations) UpdateResource(ctx context.Context, obj *uns
224224
}
225225

226226
// ApplyResource performs an apply of a unstructured resource
227-
func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate bool) (string, error) {
227+
func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force, validate, serverSideApply bool) (string, error) {
228228
span := k.tracer.StartSpan("ApplyResource")
229229
span.SetBaggageItem("kind", obj.GetKind())
230230
span.SetBaggageItem("name", obj.GetName())
@@ -237,15 +237,15 @@ func (k *kubectlResourceOperations) ApplyResource(ctx context.Context, obj *unst
237237
}
238238
defer cleanup()
239239

240-
applyOpts, err := k.newApplyOptions(ioStreams, obj, fileName, validate, force, dryRunStrategy)
240+
applyOpts, err := k.newApplyOptions(ioStreams, obj, fileName, validate, force, serverSideApply, dryRunStrategy)
241241
if err != nil {
242242
return err
243243
}
244244
return applyOpts.Run()
245245
})
246246
}
247247

248-
func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.IOStreams, obj *unstructured.Unstructured, fileName string, validate bool, force bool, dryRunStrategy cmdutil.DryRunStrategy) (*apply.ApplyOptions, error) {
248+
func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.IOStreams, obj *unstructured.Unstructured, fileName string, validate bool, force, serverSideApply bool, dryRunStrategy cmdutil.DryRunStrategy) (*apply.ApplyOptions, error) {
249249
flags := apply.NewApplyFlags(k.fact, ioStreams)
250250
o := &apply.ApplyOptions{
251251
IOStreams: ioStreams,
@@ -255,6 +255,7 @@ func (k *kubectlResourceOperations) newApplyOptions(ioStreams genericclioptions.
255255
PrintFlags: flags.PrintFlags,
256256
Overwrite: true,
257257
OpenAPIPatch: true,
258+
ServerSideApply: serverSideApply,
258259
}
259260
dynamicClient, err := dynamic.NewForConfig(k.config)
260261
if err != nil {

0 commit comments

Comments
 (0)