diff --git a/pkg/sync/common/types.go b/pkg/sync/common/types.go index eb851af83..7399cc78a 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 --force flag, delete and re-create + SyncOptionForce = "Force=true" // Sync option that enables use of --server-side flag instead of client-side SyncOptionServerSideApply = "ServerSideApply=true" // Sync option that disables resource deletion diff --git a/pkg/sync/sync_context.go b/pkg/sync/sync_context.go index 6561a9e10..5f8153cf8 100644 --- a/pkg/sync/sync_context.go +++ b/pkg/sync/sync_context.go @@ -913,7 +913,7 @@ func getDryRunStrategy(serverSideApply, dryRun bool) cmdutil.DryRunStrategy { return cmdutil.DryRunClient } -func (sc *syncContext) applyObject(t *syncTask, dryRun, force, validate bool) (common.ResultCode, string) { +func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) { serverSideApply := sc.serverSideApply || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply) dryRunStrategy := getDryRunStrategy(serverSideApply, dryRun) @@ -921,6 +921,7 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, force, validate bool) (c var err error var message string shouldReplace := sc.replace || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionReplace) + force := sc.force || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionForce) applyFn := func(dryRunStrategy cmdutil.DryRunStrategy) (string, error) { if !shouldReplace { return sc.resourceOps.ApplyResource(context.TODO(), t.targetObj, dryRunStrategy, force, validate, serverSideApply, sc.serverSideApplyManager) @@ -1205,7 +1206,7 @@ func (sc *syncContext) processCreateTasks(state runState, tasks syncTasks, dryRu logCtx := sc.log.WithValues("dryRun", dryRun, "task", t) logCtx.V(1).Info("Applying") validate := sc.validate && !resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionsDisableValidation) - result, message := sc.applyObject(t, dryRun, sc.force, validate) + result, message := sc.applyObject(t, dryRun, validate) if result == common.ResultCodeSyncFailed { logCtx.WithValues("message", message).Info("Apply failed") state = failed diff --git a/pkg/sync/sync_context_test.go b/pkg/sync/sync_context_test.go index 984787014..00163800c 100644 --- a/pkg/sync/sync_context_test.go +++ b/pkg/sync/sync_context_test.go @@ -5,12 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "k8s.io/kubectl/pkg/cmd/util" "net/http" "net/http/httptest" "reflect" "testing" + "k8s.io/kubectl/pkg/cmd/util" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" @@ -859,6 +860,52 @@ func TestSync_ServerSideApply(t *testing.T) { } } +func withForceAnnotation(un *unstructured.Unstructured) *unstructured.Unstructured { + un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: synccommon.SyncOptionForce}) + return un +} + +func withForceAndReplaceAnnotations(un *unstructured.Unstructured) *unstructured.Unstructured { + un.SetAnnotations(map[string]string{synccommon.AnnotationSyncOptions: "Force=true,Replace=true"}) + return un +} + +func TestSync_Force(t *testing.T) { + testCases := []struct { + name string + target *unstructured.Unstructured + live *unstructured.Unstructured + commandUsed string + force bool + }{ + {"NoAnnotation", NewPod(), NewPod(), "apply", false}, + {"ForceApplyAnnotationIsSet", withForceAnnotation(NewPod()), NewPod(), "apply", true}, + {"ForceReplaceAnnotationIsSet", withForceAndReplaceAnnotations(NewPod()), NewPod(), "replace", true}, + {"LiveObjectMissing", withReplaceAnnotation(NewPod()), nil, "create", false}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + syncCtx := newTestSyncCtx(nil) + + 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() + + resourceOps, _ := syncCtx.resourceOps.(*kubetest.MockResourceOps) + assert.Equal(t, tc.commandUsed, resourceOps.GetLastResourceCommand(kube.GetResourceKey(tc.target))) + assert.Equal(t, tc.force, resourceOps.GetLastForce()) + }) + } +} + func TestSelectiveSyncOnly(t *testing.T) { pod1 := NewPod() pod1.SetName("pod-1") diff --git a/pkg/utils/kube/kubetest/mock_resource_operations.go b/pkg/utils/kube/kubetest/mock_resource_operations.go index 31ea6a0e4..2e55dd1f6 100644 --- a/pkg/utils/kube/kubetest/mock_resource_operations.go +++ b/pkg/utils/kube/kubetest/mock_resource_operations.go @@ -22,6 +22,7 @@ type MockResourceOps struct { lastValidate bool serverSideApply bool serverSideApplyManager string + lastForce bool recordLock sync.RWMutex @@ -73,6 +74,19 @@ func (r *MockResourceOps) SetLastServerSideApplyManager(manager string) { r.recordLock.Unlock() } +func (r *MockResourceOps) SetLastForce(force bool) { + r.recordLock.Lock() + r.lastForce = force + r.recordLock.Unlock() +} + +func (r *MockResourceOps) GetLastForce() bool { + r.recordLock.RLock() + force := r.lastForce + r.recordLock.RUnlock() + return force +} + func (r *MockResourceOps) SetLastResourceCommand(key kube.ResourceKey, cmd string) { r.recordLock.Lock() if r.lastCommandPerResource == nil { @@ -95,6 +109,7 @@ func (r *MockResourceOps) ApplyResource(ctx context.Context, obj *unstructured.U r.SetLastValidate(validate) r.SetLastServerSideApply(serverSideApply) r.SetLastServerSideApplyManager(manager) + r.SetLastForce(force) r.SetLastResourceCommand(kube.GetResourceKey(obj), "apply") command, ok := r.Commands[obj.GetName()] if !ok { @@ -105,9 +120,9 @@ func (r *MockResourceOps) ApplyResource(ctx context.Context, obj *unstructured.U } func (r *MockResourceOps) ReplaceResource(ctx context.Context, obj *unstructured.Unstructured, dryRunStrategy cmdutil.DryRunStrategy, force bool) (string, error) { + r.SetLastForce(force) command, ok := r.Commands[obj.GetName()] r.SetLastResourceCommand(kube.GetResourceKey(obj), "replace") - if !ok { return "", nil }