@@ -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
216228func 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+
10751137func (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.
0 commit comments