Skip to content

Commit 69dfa70

Browse files
authored
feat: auto migrate kubectl-client-side-apply fields for SSA (#727)
* feat: auto migrate kubectl-client-side-apply fields for SSA Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * fix master version Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * run gofumpt Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * Propagate sync error instead of logging Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * allow enable/disable of CSA migration using annotation Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * fix linting Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * Refactor to allow for multiple managers and disable option Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * remove commentj Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * refactor Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * fix test Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * Add docs for client side apply migration Signed-off-by: Peter Jiang <peterjiang823@gmail.com> * Edit comment Signed-off-by: Peter Jiang <peterjiang823@gmail.com> --------- Signed-off-by: Peter Jiang <peterjiang823@gmail.com>
1 parent cebed7e commit 69dfa70

File tree

3 files changed

+187
-29
lines changed

3 files changed

+187
-29
lines changed

pkg/sync/common/types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ const (
4444
SyncOptionDeleteRequireConfirm = "Delete=confirm"
4545
// Sync option that requires confirmation before deleting the resource
4646
SyncOptionPruneRequireConfirm = "Prune=confirm"
47+
// Sync option that enables client-side apply migration
48+
SyncOptionClientSideApplyMigration = "ClientSideApplyMigration=true"
49+
// Sync option that disables client-side apply migration
50+
SyncOptionDisableClientSideApplyMigration = "ClientSideApplyMigration=false"
51+
52+
// Default field manager for client-side apply migration
53+
DefaultClientSideApplyMigrationManager = "kubectl-client-side-apply"
4754
)
4855

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

pkg/sync/sync_context.go

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@ func WithServerSideApplyManager(manager string) SyncOpt {
212212
}
213213
}
214214

215+
// WithClientSideApplyMigration configures client-side apply migration for server-side apply.
216+
// When enabled, fields managed by the specified manager will be migrated to server-side apply.
217+
// Defaults to enabled=true with manager="kubectl-client-side-apply" if not configured.
218+
func WithClientSideApplyMigration(enabled bool, manager string) SyncOpt {
219+
return func(ctx *syncContext) {
220+
ctx.enableClientSideApplyMigration = enabled
221+
if enabled && manager != "" {
222+
ctx.clientSideApplyMigrationManager = manager
223+
}
224+
}
225+
}
226+
215227
// NewSyncContext creates new instance of a SyncContext
216228
func NewSyncContext(
217229
revision string,
@@ -240,21 +252,23 @@ func NewSyncContext(
240252
return nil, nil, fmt.Errorf("failed to manage resources: %w", err)
241253
}
242254
ctx := &syncContext{
243-
revision: revision,
244-
resources: groupResources(reconciliationResult),
245-
hooks: reconciliationResult.Hooks,
246-
config: restConfig,
247-
rawConfig: rawConfig,
248-
dynamicIf: dynamicIf,
249-
disco: disco,
250-
extensionsclientset: extensionsclientset,
251-
kubectl: kubectl,
252-
resourceOps: resourceOps,
253-
namespace: namespace,
254-
log: textlogger.NewLogger(textlogger.NewConfig()),
255-
validate: true,
256-
startedAt: time.Now(),
257-
syncRes: map[string]common.ResourceSyncResult{},
255+
revision: revision,
256+
resources: groupResources(reconciliationResult),
257+
hooks: reconciliationResult.Hooks,
258+
config: restConfig,
259+
rawConfig: rawConfig,
260+
dynamicIf: dynamicIf,
261+
disco: disco,
262+
extensionsclientset: extensionsclientset,
263+
kubectl: kubectl,
264+
resourceOps: resourceOps,
265+
namespace: namespace,
266+
log: textlogger.NewLogger(textlogger.NewConfig()),
267+
validate: true,
268+
startedAt: time.Now(),
269+
syncRes: map[string]common.ResourceSyncResult{},
270+
clientSideApplyMigrationManager: common.DefaultClientSideApplyMigrationManager,
271+
enableClientSideApplyMigration: true,
258272
permissionValidator: func(_ *unstructured.Unstructured, _ *metav1.APIResource) error {
259273
return nil
260274
},
@@ -346,20 +360,22 @@ type syncContext struct {
346360
resourceOps kubeutil.ResourceOperations
347361
namespace string
348362

349-
dryRun bool
350-
skipDryRun bool
351-
skipDryRunOnMissingResource bool
352-
force bool
353-
validate bool
354-
skipHooks bool
355-
resourcesFilter func(key kubeutil.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool
356-
prune bool
357-
replace bool
358-
serverSideApply bool
359-
serverSideApplyManager string
360-
pruneLast bool
361-
prunePropagationPolicy *metav1.DeletionPropagation
362-
pruneConfirmed bool
363+
dryRun bool
364+
skipDryRun bool
365+
skipDryRunOnMissingResource bool
366+
force bool
367+
validate bool
368+
skipHooks bool
369+
resourcesFilter func(key kubeutil.ResourceKey, target *unstructured.Unstructured, live *unstructured.Unstructured) bool
370+
prune bool
371+
replace bool
372+
serverSideApply bool
373+
serverSideApplyManager string
374+
pruneLast bool
375+
prunePropagationPolicy *metav1.DeletionPropagation
376+
pruneConfirmed bool
377+
clientSideApplyMigrationManager string
378+
enableClientSideApplyMigration bool
363379

364380
syncRes map[string]common.ResourceSyncResult
365381
startedAt time.Time
@@ -1072,6 +1088,52 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
10721088
return sc.serverSideApply || resourceutil.HasAnnotationOption(targetObj, common.AnnotationSyncOptions, common.SyncOptionServerSideApply)
10731089
}
10741090

1091+
// needsClientSideApplyMigration checks if a resource has fields managed by the specified manager
1092+
// that need to be migrated to the server-side apply manager
1093+
func (sc *syncContext) needsClientSideApplyMigration(liveObj *unstructured.Unstructured, fieldManager string) bool {
1094+
if liveObj == nil || fieldManager == "" {
1095+
return false
1096+
}
1097+
1098+
managedFields := liveObj.GetManagedFields()
1099+
if len(managedFields) == 0 {
1100+
return false
1101+
}
1102+
1103+
for _, field := range managedFields {
1104+
if field.Manager == fieldManager {
1105+
return true
1106+
}
1107+
}
1108+
1109+
return false
1110+
}
1111+
1112+
// performClientSideApplyMigration performs a client-side-apply using the specified field manager.
1113+
// This moves the 'last-applied-configuration' field to be managed by the specified manager.
1114+
// The next time server-side apply is performed, kubernetes automatically migrates all fields from the manager
1115+
// that owns 'last-applied-configuration' to the manager that uses server-side apply. This will remove the
1116+
// specified manager from the resources managed fields. 'kubectl-client-side-apply' is used as the default manager.
1117+
func (sc *syncContext) performClientSideApplyMigration(targetObj *unstructured.Unstructured, fieldManager string) error {
1118+
sc.log.WithValues("resource", kubeutil.GetResourceKey(targetObj)).V(1).Info("Performing client-side apply migration step")
1119+
1120+
// Apply with the specified manager to set up the migration
1121+
_, err := sc.resourceOps.ApplyResource(
1122+
context.TODO(),
1123+
targetObj,
1124+
cmdutil.DryRunNone,
1125+
false,
1126+
false,
1127+
false,
1128+
fieldManager,
1129+
)
1130+
if err != nil {
1131+
return fmt.Errorf("failed to perform client-side apply migration on manager %s: %w", fieldManager, err)
1132+
}
1133+
1134+
return nil
1135+
}
1136+
10751137
func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.ResultCode, string) {
10761138
dryRunStrategy := cmdutil.DryRunNone
10771139
if dryRun {
@@ -1088,6 +1150,17 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
10881150
shouldReplace := sc.replace || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionReplace)
10891151
force := sc.force || resourceutil.HasAnnotationOption(t.targetObj, common.AnnotationSyncOptions, common.SyncOptionForce)
10901152
serverSideApply := sc.shouldUseServerSideApply(t.targetObj, dryRun)
1153+
1154+
// Check if we need to perform client-side apply migration for server-side apply
1155+
if serverSideApply && !dryRun && sc.enableClientSideApplyMigration {
1156+
if sc.needsClientSideApplyMigration(t.liveObj, sc.clientSideApplyMigrationManager) {
1157+
err = sc.performClientSideApplyMigration(t.targetObj, sc.clientSideApplyMigrationManager)
1158+
if err != nil {
1159+
return common.ResultCodeSyncFailed, fmt.Sprintf("Failed to perform client-side apply migration: %v", err)
1160+
}
1161+
}
1162+
}
1163+
10911164
if shouldReplace {
10921165
if t.liveObj != nil {
10931166
// Avoid using `kubectl replace` for CRDs since 'replace' might recreate resource and so delete all CRD instances.

pkg/sync/sync_context_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2184,3 +2184,81 @@ func BenchmarkSync(b *testing.B) {
21842184
syncCtx.Sync()
21852185
}
21862186
}
2187+
2188+
func TestNeedsClientSideApplyMigration(t *testing.T) {
2189+
syncCtx := newTestSyncCtx(nil)
2190+
2191+
tests := []struct {
2192+
name string
2193+
liveObj *unstructured.Unstructured
2194+
expected bool
2195+
}{
2196+
{
2197+
name: "nil object",
2198+
liveObj: nil,
2199+
expected: false,
2200+
},
2201+
{
2202+
name: "object with no managed fields",
2203+
liveObj: testingutils.NewPod(),
2204+
expected: false,
2205+
},
2206+
{
2207+
name: "object with kubectl-client-side-apply fields",
2208+
liveObj: func() *unstructured.Unstructured {
2209+
obj := testingutils.NewPod()
2210+
obj.SetManagedFields([]metav1.ManagedFieldsEntry{
2211+
{
2212+
Manager: "kubectl-client-side-apply",
2213+
Operation: metav1.ManagedFieldsOperationUpdate,
2214+
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:metadata":{"f:annotations":{}}}`)},
2215+
},
2216+
})
2217+
return obj
2218+
}(),
2219+
expected: true,
2220+
},
2221+
{
2222+
name: "object with only argocd-controller fields",
2223+
liveObj: func() *unstructured.Unstructured {
2224+
obj := testingutils.NewPod()
2225+
obj.SetManagedFields([]metav1.ManagedFieldsEntry{
2226+
{
2227+
Manager: "argocd-controller",
2228+
Operation: metav1.ManagedFieldsOperationApply,
2229+
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
2230+
},
2231+
})
2232+
return obj
2233+
}(),
2234+
expected: false,
2235+
},
2236+
{
2237+
name: "object with mixed field managers",
2238+
liveObj: func() *unstructured.Unstructured {
2239+
obj := testingutils.NewPod()
2240+
obj.SetManagedFields([]metav1.ManagedFieldsEntry{
2241+
{
2242+
Manager: "kubectl-client-side-apply",
2243+
Operation: metav1.ManagedFieldsOperationUpdate,
2244+
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:metadata":{"f:annotations":{}}}`)},
2245+
},
2246+
{
2247+
Manager: "argocd-controller",
2248+
Operation: metav1.ManagedFieldsOperationApply,
2249+
FieldsV1: &metav1.FieldsV1{Raw: []byte(`{"f:spec":{"f:replicas":{}}}`)},
2250+
},
2251+
})
2252+
return obj
2253+
}(),
2254+
expected: true,
2255+
},
2256+
}
2257+
2258+
for _, tt := range tests {
2259+
t.Run(tt.name, func(t *testing.T) {
2260+
result := syncCtx.needsClientSideApplyMigration(tt.liveObj, "kubectl-client-side-apply")
2261+
assert.Equal(t, tt.expected, result)
2262+
})
2263+
}
2264+
}

0 commit comments

Comments
 (0)