@@ -212,6 +212,18 @@ func WithServerSideApplyManager(manager string) SyncOpt {
212
212
}
213
213
}
214
214
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
+
215
227
// NewSyncContext creates new instance of a SyncContext
216
228
func NewSyncContext (
217
229
revision string ,
@@ -240,21 +252,23 @@ func NewSyncContext(
240
252
return nil , nil , fmt .Errorf ("failed to manage resources: %w" , err )
241
253
}
242
254
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 ,
258
272
permissionValidator : func (_ * unstructured.Unstructured , _ * metav1.APIResource ) error {
259
273
return nil
260
274
},
@@ -346,20 +360,22 @@ type syncContext struct {
346
360
resourceOps kubeutil.ResourceOperations
347
361
namespace string
348
362
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
363
379
364
380
syncRes map [string ]common.ResourceSyncResult
365
381
startedAt time.Time
@@ -1072,6 +1088,52 @@ func (sc *syncContext) shouldUseServerSideApply(targetObj *unstructured.Unstruct
1072
1088
return sc .serverSideApply || resourceutil .HasAnnotationOption (targetObj , common .AnnotationSyncOptions , common .SyncOptionServerSideApply )
1073
1089
}
1074
1090
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
+
1075
1137
func (sc * syncContext ) applyObject (t * syncTask , dryRun , validate bool ) (common.ResultCode , string ) {
1076
1138
dryRunStrategy := cmdutil .DryRunNone
1077
1139
if dryRun {
@@ -1088,6 +1150,17 @@ func (sc *syncContext) applyObject(t *syncTask, dryRun, validate bool) (common.R
1088
1150
shouldReplace := sc .replace || resourceutil .HasAnnotationOption (t .targetObj , common .AnnotationSyncOptions , common .SyncOptionReplace )
1089
1151
force := sc .force || resourceutil .HasAnnotationOption (t .targetObj , common .AnnotationSyncOptions , common .SyncOptionForce )
1090
1152
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
+
1091
1164
if shouldReplace {
1092
1165
if t .liveObj != nil {
1093
1166
// Avoid using `kubectl replace` for CRDs since 'replace' might recreate resource and so delete all CRD instances.
0 commit comments