feat: add SSA field manager migration options (#23337)

Signed-off-by: Peter Jiang <peterjiang823@gmail.com>
Signed-off-by: Peter Jiang <35584807+pjiang-dev@users.noreply.github.com>
This commit is contained in:
Peter Jiang 2025-06-13 14:58:07 -07:00 committed by GitHub
parent 7a064000a0
commit dc1d148a5d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 155 additions and 0 deletions

View file

@ -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"

View file

@ -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
@ -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)),
}

View file

@ -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

View file

@ -293,6 +293,44 @@ to apply changes.
Note: [`Replace=true`](#replace-resource-instead-of-applying-changes) takes precedence over `ServerSideApply=true`.
### Client-Side Apply Migration
Argo CD supports client-side apply migration, which helps transitioning from client-side apply to server-side apply by moving a resource's 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.
### How it works
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.