@@ -439,37 +439,106 @@ func (bc *Boxcutter) createOrUpdate(ctx context.Context, user user.Info, rev *oc
439439 return err
440440 }
441441
442- // DEPRECATION NOTICE: Using client.Apply (deprecated in controller-runtime v0.23.0+)
442+ // Build apply configuration by manually constructing an unstructured object.
443443 //
444- // WHY WE CAN'T FIX THIS YET:
445- // The recommended replacement is the new typed Apply() method that requires generated
446- // apply configurations (ApplyConfiguration types). However, this project does not
447- // currently generate these apply configurations for its API types .
444+ // We cannot safely convert the typed ClusterExtensionRevision struct directly to unstructured
445+ // because that would serialize all fields, including zero values for fields without omitempty tags.
446+ // This would incorrectly claim field ownership for fields we don't manage, violating Server-Side
447+ // Apply semantics .
448448 //
449- // WHY WE NEED SERVER-SIDE APPLY SEMANTICS:
450- // This controller requires server-side apply with field ownership management to:
451- // 1. Track which controller owns which fields (via client.FieldOwner)
452- // 2. Take ownership of fields from other managers during upgrades (via client.ForceOwnership)
453- // 3. Automatically create-or-update without explicit Get/Create/Update logic
454- //
455- // WHY ALTERNATIVES DON'T WORK:
456- // - client.MergeFrom(): Lacks field ownership - causes conflicts during controller upgrades
457- // - client.StrategicMergePatch(): No field management - upgrade tests fail with ownership errors
458- // - Manual Create/Update: Loses server-side apply benefits, complex to implement correctly
459- //
460- // WHAT'S REQUIRED TO FIX PROPERLY:
461- // 1. Generate apply configurations for all API types (ClusterExtensionRevision, etc.)
462- // - Requires running controller-gen with --with-applyconfig flag
463- // - Generates ClusterExtensionRevisionApplyConfiguration types
464- // 2. Update all resource creation/update code to use typed Apply methods
465- // 3. Update all tests to work with new patterns
466- // This is a project-wide effort beyond the scope of the k8s v1.35 upgrade.
467- //
468- // MIGRATION PATH:
469- // Track in a future issue: "Generate apply configurations and migrate to typed Apply methods"
470- //
471- // nolint:staticcheck // SA1019: server-side apply required, needs generated apply configurations
472- return bc .Client .Patch (ctx , rev , client .Apply , client .FieldOwner (bc .FieldOwner ), client .ForceOwnership )
449+ // Instead, we manually construct the unstructured object with only the fields we explicitly manage.
450+ u := & unstructured.Unstructured {
451+ Object : map [string ]interface {}{
452+ "apiVersion" : rev .APIVersion ,
453+ "kind" : rev .Kind ,
454+ "metadata" : map [string ]interface {}{
455+ "name" : rev .Name ,
456+ },
457+ "spec" : map [string ]interface {}{
458+ "revision" : rev .Spec .Revision ,
459+ "phases" : convertPhasesToUnstructured (rev .Spec .Phases ),
460+ },
461+ },
462+ }
463+
464+ // Only set optional fields if they have non-zero values
465+ if rev .Spec .LifecycleState != "" {
466+ spec := u .Object ["spec" ].(map [string ]interface {})
467+ spec ["lifecycleState" ] = rev .Spec .LifecycleState
468+ }
469+ if rev .Spec .ProgressDeadlineMinutes > 0 {
470+ spec := u .Object ["spec" ].(map [string ]interface {})
471+ spec ["progressDeadlineMinutes" ] = rev .Spec .ProgressDeadlineMinutes
472+ }
473+
474+ // Add metadata fields if present
475+ if len (rev .Labels ) > 0 {
476+ metadata := u .Object ["metadata" ].(map [string ]interface {})
477+ metadata ["labels" ] = rev .Labels
478+ }
479+ if len (rev .Annotations ) > 0 {
480+ metadata := u .Object ["metadata" ].(map [string ]interface {})
481+ metadata ["annotations" ] = rev .Annotations
482+ }
483+ if len (rev .OwnerReferences ) > 0 {
484+ metadata := u .Object ["metadata" ].(map [string ]interface {})
485+ metadata ["ownerReferences" ] = convertOwnerReferencesToUnstructured (rev .OwnerReferences )
486+ }
487+
488+ applyConfig := client .ApplyConfigurationFromUnstructured (u )
489+
490+ // Use Server-Side Apply with field ownership to manage the revision.
491+ return bc .Client .Apply (ctx , applyConfig , client .FieldOwner (bc .FieldOwner ), client .ForceOwnership )
492+ }
493+
494+ // convertPhasesToUnstructured converts phases to unstructured format, including only managed fields.
495+ func convertPhasesToUnstructured (phases []ocv1.ClusterExtensionRevisionPhase ) []interface {} {
496+ if len (phases ) == 0 {
497+ return nil
498+ }
499+
500+ result := make ([]interface {}, 0 , len (phases ))
501+ for _ , phase := range phases {
502+ objects := make ([]interface {}, 0 , len (phase .Objects ))
503+ for _ , obj := range phase .Objects {
504+ objMap := map [string ]interface {}{
505+ "object" : obj .Object .Object ,
506+ }
507+ // Only include collisionProtection if non-empty
508+ if obj .CollisionProtection != "" {
509+ objMap ["collisionProtection" ] = obj .CollisionProtection
510+ }
511+ objects = append (objects , objMap )
512+ }
513+
514+ result = append (result , map [string ]interface {}{
515+ "name" : phase .Name ,
516+ "objects" : objects ,
517+ })
518+ }
519+ return result
520+ }
521+
522+ // convertOwnerReferencesToUnstructured converts owner references to unstructured format.
523+ func convertOwnerReferencesToUnstructured (ownerRefs []metav1.OwnerReference ) []interface {} {
524+ result := make ([]interface {}, 0 , len (ownerRefs ))
525+ for _ , ref := range ownerRefs {
526+ refMap := map [string ]interface {}{
527+ "apiVersion" : ref .APIVersion ,
528+ "kind" : ref .Kind ,
529+ "name" : ref .Name ,
530+ "uid" : ref .UID ,
531+ }
532+ // Only include optional fields if set
533+ if ref .Controller != nil {
534+ refMap ["controller" ] = * ref .Controller
535+ }
536+ if ref .BlockOwnerDeletion != nil {
537+ refMap ["blockOwnerDeletion" ] = * ref .BlockOwnerDeletion
538+ }
539+ result = append (result , refMap )
540+ }
541+ return result
473542}
474543
475544func (bc * Boxcutter ) Apply (ctx context.Context , contentFS fs.FS , ext * ocv1.ClusterExtension , objectLabels , revisionAnnotations map [string ]string ) (bool , string , error ) {
0 commit comments