Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ const (
// AnnotationCompareOptions is a comma-separated list of options for comparison
AnnotationCompareOptions = "argocd.argoproj.io/compare-options"

// AnnotationClientSideApplyMigrationManager specifies a custom field manager for client-side apply migration
AnnotationClientSideApplyMigrationManager = "argocd.argoproj.io/client-side-apply-migration-manager"

// AnnotationIgnoreHealthCheck when set on an Application's immediate child indicates that its health check
// can be disregarded.
AnnotationIgnoreHealthCheck = "argocd.argoproj.io/ignore-healthcheck"
Expand Down
10 changes: 10 additions & 0 deletions controller/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
prunePropagationPolicy = metav1.DeletePropagationOrphan
}

clientSideApplyManager := common.DefaultClientSideApplyMigrationManager
// Check for custom field manager from application annotation
if managerValue := app.GetAnnotation(cdcommon.AnnotationClientSideApplyMigrationManager); managerValue != "" {
clientSideApplyManager = managerValue
}

openAPISchema, err := m.getOpenAPISchema(destCluster)
if err != nil {
state.Phase = common.OperationError
Expand Down Expand Up @@ -376,6 +382,10 @@ func (m *appStateManager) SyncAppState(app *v1alpha1.Application, state *v1alpha
sync.WithReplace(syncOp.SyncOptions.HasOption(common.SyncOptionReplace)),
sync.WithServerSideApply(syncOp.SyncOptions.HasOption(common.SyncOptionServerSideApply)),
sync.WithServerSideApplyManager(cdcommon.ArgoCDSSAManager),
sync.WithClientSideApplyMigration(
!syncOp.SyncOptions.HasOption(common.SyncOptionDisableClientSideApplyMigration),
clientSideApplyManager,
),
sync.WithPruneConfirmed(app.IsDeletionConfirmed(state.StartedAt.Time)),
sync.WithSkipDryRunOnMissingResource(syncOp.SyncOptions.HasOption(common.SyncOptionSkipDryRunOnMissingResource)),
}
Expand Down
104 changes: 104 additions & 0 deletions controller/sync_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1412,6 +1412,110 @@ func TestSyncWithImpersonate(t *testing.T) {
})
}

func TestClientSideApplyMigration(t *testing.T) {
t.Parallel()

type fixture struct {
application *v1alpha1.Application
controller *ApplicationController
}

setup := func(disableMigration bool, customManager string) *fixture {
app := newFakeApp()
app.Status.OperationState = nil
app.Status.History = nil

// Add sync options
if disableMigration {
app.Spec.SyncPolicy.SyncOptions = append(app.Spec.SyncPolicy.SyncOptions, "DisableClientSideApplyMigration=true")
}

// Add custom manager annotation if specified
if customManager != "" {
app.Annotations = map[string]string{
"argocd.argoproj.io/client-side-apply-migration-manager": customManager,
}
}

project := &v1alpha1.AppProject{
ObjectMeta: metav1.ObjectMeta{
Namespace: test.FakeArgoCDNamespace,
Name: "default",
},
}
data := fakeData{
apps: []runtime.Object{app, project},
manifestResponse: &apiclient.ManifestResponse{
Manifests: []string{},
Namespace: test.FakeDestNamespace,
Server: test.FakeClusterURL,
Revision: "abc123",
},
managedLiveObjs: make(map[kube.ResourceKey]*unstructured.Unstructured),
}
ctrl := newFakeController(&data, nil)

return &fixture{
application: app,
controller: ctrl,
}
}

t.Run("client-side apply migration enabled by default", func(t *testing.T) {
// given
t.Parallel()
f := setup(false, "")

// when
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
}}
f.controller.appStateManager.SyncAppState(f.application, opState)

// then
assert.Equal(t, common.OperationSucceeded, opState.Phase)
assert.Contains(t, opState.Message, "successfully synced")
})

t.Run("client-side apply migration disabled", func(t *testing.T) {
// given
t.Parallel()
f := setup(true, "")

// when
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
}}
f.controller.appStateManager.SyncAppState(f.application, opState)

// then
assert.Equal(t, common.OperationSucceeded, opState.Phase)
assert.Contains(t, opState.Message, "successfully synced")
})

t.Run("client-side apply migration with custom manager", func(t *testing.T) {
// given
t.Parallel()
f := setup(false, "my-custom-manager")

// when
opState := &v1alpha1.OperationState{Operation: v1alpha1.Operation{
Sync: &v1alpha1.SyncOperation{
Source: &v1alpha1.ApplicationSource{},
},
}}
f.controller.appStateManager.SyncAppState(f.application, opState)

// then
assert.Equal(t, common.OperationSucceeded, opState.Phase)
assert.Contains(t, opState.Message, "successfully synced")
})
}

func dig[T any](obj any, path []any) T {
i := obj

Expand Down
34 changes: 34 additions & 0 deletions docs/user-guide/sync-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,40 @@ to apply changes.

Note: [`Replace=true`](#replace-resource-instead-of-applying-changes) takes precedence over `ServerSideApply=true`.

Argo CD supports client-side apply migration, which helps transition from client-side apply to server-side apply by moving a resources' managed fields from one manager to Argo CD's manager. This feature is particularly useful when you need to migrate existing resources that were created using kubectl client-side apply to server-side apply with Argo CD.

By default, client-side apply migration is enabled. You can disable it using the sync option:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
syncPolicy:
syncOptions:
- DisableClientSideApplyMigration=true
```

You can specify a custom field manager for the client-side apply migration using an annotation:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
annotations:
argocd.argoproj.io/client-side-apply-migration-manager: "my-custom-manager"
```

This is useful when you have other operators managing resources that are no longer in use and would like Argo CD to own all the fields for that operator.

When client-side apply migration is enabled:
1. Argo CD will use the specified field manager (or default if not specified) to perform migration
2. During a server-side apply sync operation, it will:
- Perfirm a client-side-apply with the specified field manager
- Move the 'last-appled-configuration' annotation to be managed by the specified manager
- Perform the server-side apply, which will auto migrate all the fields under the manager that owns the 'last-applied-configration' annotation.

This feature is based on Kubernetes' [client-side apply migration KEP](https://github.com/alexzielenski/enhancements/blob/03df8820b9feca6d2cab78e303c99b2c9c0c4c5c/keps/sig-cli/3517-kubectl-client-side-apply-migration/README.md), which provides the auto migration from client-side to server-side apply.

## Fail the sync if a shared resource is found

By default, Argo CD will apply all manifests found in the git path configured in the Application regardless if the resources defined in the yamls are already applied by another Application. If the `FailOnSharedResource` sync option is set, Argo CD will fail the sync whenever it finds a resource in the current Application that is already applied in the cluster by another Application.
Expand Down
Loading