@@ -152,22 +152,12 @@ func (c *ClusterObjectSetReconciler) reconcile(ctx context.Context, cos *ocv1.Cl
152152 return ctrl.Result {}, nil
153153 }
154154
155- phases , opts , err := c .buildBoxcutterPhases (ctx , cos )
155+ phases , currentPhases , opts , err := c .buildBoxcutterPhases (ctx , cos )
156156 if err != nil {
157157 setRetryingConditions (cos , err .Error ())
158158 return ctrl.Result {}, fmt .Errorf ("converting to boxcutter revision: %v" , err )
159159 }
160160
161- currentPhases := make ([]ocv1.ObservedPhase , 0 , len (phases ))
162- for _ , phase := range phases {
163- hash , err := computePhaseDigest (phase )
164- if err != nil {
165- setRetryingConditions (cos , err .Error ())
166- return ctrl.Result {}, fmt .Errorf ("computing phase digest: %v" , err )
167- }
168- currentPhases = append (currentPhases , ocv1.ObservedPhase {Name : phase .GetName (), Digest : hash })
169- }
170-
171161 if len (cos .Status .ObservedPhases ) == 0 {
172162 cos .Status .ObservedPhases = currentPhases
173163 } else if err := verifyObservedPhases (cos .Status .ObservedPhases , currentPhases ); err != nil {
@@ -488,10 +478,10 @@ func (c *ClusterObjectSetReconciler) listPreviousRevisions(ctx context.Context,
488478 return previous , nil
489479}
490480
491- func (c * ClusterObjectSetReconciler ) buildBoxcutterPhases (ctx context.Context , cos * ocv1.ClusterObjectSet ) ([]boxcutter.Phase , []boxcutter.RevisionReconcileOption , error ) {
481+ func (c * ClusterObjectSetReconciler ) buildBoxcutterPhases (ctx context.Context , cos * ocv1.ClusterObjectSet ) ([]boxcutter.Phase , []ocv1. ObservedPhase , [] boxcutter.RevisionReconcileOption , error ) {
492482 previous , err := c .listPreviousRevisions (ctx , cos )
493483 if err != nil {
494- return nil , nil , fmt .Errorf ("listing previous revisions: %w" , err )
484+ return nil , nil , nil , fmt .Errorf ("listing previous revisions: %w" , err )
495485 }
496486
497487 // Convert to []client.Object for boxcutter
@@ -502,7 +492,7 @@ func (c *ClusterObjectSetReconciler) buildBoxcutterPhases(ctx context.Context, c
502492
503493 progressionProbes , err := buildProgressionProbes (cos .Spec .ProgressionProbes )
504494 if err != nil {
505- return nil , nil , err
495+ return nil , nil , nil , err
506496 }
507497
508498 opts := []boxcutter.RevisionReconcileOption {
@@ -511,9 +501,10 @@ func (c *ClusterObjectSetReconciler) buildBoxcutterPhases(ctx context.Context, c
511501 boxcutter .WithAggregatePhaseReconcileErrors (),
512502 }
513503
514- phases := make ([]boxcutter.Phase , 0 )
504+ phases := make ([]boxcutter.Phase , 0 , len (cos .Spec .Phases ))
505+ observedPhases := make ([]ocv1.ObservedPhase , 0 , len (cos .Spec .Phases ))
515506 for _ , specPhase := range cos .Spec .Phases {
516- objs := make ([]client.Object , 0 )
507+ objs := make ([]client.Object , 0 , len ( specPhase . Objects ) )
517508 for _ , specObj := range specPhase .Objects {
518509 var obj * unstructured.Unstructured
519510 switch {
@@ -522,31 +513,42 @@ func (c *ClusterObjectSetReconciler) buildBoxcutterPhases(ctx context.Context, c
522513 case specObj .Ref .Name != "" :
523514 resolved , err := c .resolveObjectRef (ctx , specObj .Ref )
524515 if err != nil {
525- return nil , nil , fmt .Errorf ("resolving ref in phase %q: %w" , specPhase .Name , err )
516+ return nil , nil , nil , fmt .Errorf ("resolving ref in phase %q: %w" , specPhase .Name , err )
526517 }
527518 obj = resolved
528519 default :
529- return nil , nil , fmt .Errorf ("object in phase %q has neither object nor ref" , specPhase .Name )
520+ return nil , nil , nil , fmt .Errorf ("object in phase %q has neither object nor ref" , specPhase .Name )
530521 }
531522
523+ objs = append (objs , obj )
524+ }
525+
526+ // Compute digest from the user-provided objects before controller mutations.
527+ digest , err := computePhaseDigest (specPhase .Name , objs )
528+ if err != nil {
529+ return nil , nil , nil , fmt .Errorf ("computing phase digest: %w" , err )
530+ }
531+ observedPhases = append (observedPhases , ocv1.ObservedPhase {Name : specPhase .Name , Digest : digest })
532+
533+ // Apply controller mutations after digest computation.
534+ for i , obj := range objs {
532535 objLabels := obj .GetLabels ()
533536 if objLabels == nil {
534537 objLabels = map [string ]string {}
535538 }
536539 objLabels [labels .OwnerNameKey ] = cos .Labels [labels .OwnerNameKey ]
537540 obj .SetLabels (objLabels )
538541
539- switch cp := EffectiveCollisionProtection (cos .Spec .CollisionProtection , specPhase .CollisionProtection , specObj .CollisionProtection ); cp {
542+ switch cp := EffectiveCollisionProtection (cos .Spec .CollisionProtection , specPhase .CollisionProtection , specPhase . Objects [ i ] .CollisionProtection ); cp {
540543 case ocv1 .CollisionProtectionIfNoController , ocv1 .CollisionProtectionNone :
541544 opts = append (opts , boxcutter .WithObjectReconcileOptions (
542545 obj , boxcutter .WithCollisionProtection (cp )))
543546 }
544-
545- objs = append (objs , obj )
546547 }
548+
547549 phases = append (phases , boxcutter .NewPhase (specPhase .Name , objs ))
548550 }
549- return phases , opts , nil
551+ return phases , observedPhases , opts , nil
550552}
551553
552554// resolveObjectRef fetches the referenced Secret, reads the value at the specified key,
@@ -721,36 +723,48 @@ func markAsArchived(cos *ocv1.ClusterObjectSet) bool {
721723}
722724
723725// computePhaseDigest computes a deterministic SHA-256 digest of a phase's
724- // resolved content (name + objects). JSON serialization of unstructured
725- // objects produces a canonical encoding with sorted map keys.
726- func computePhaseDigest (phase boxcutter.Phase ) (string , error ) {
726+ // resolved content (name + objects) before any controller mutations.
727+ // JSON serialization of unstructured objects produces a canonical encoding
728+ // with sorted map keys.
729+ func computePhaseDigest (name string , objects []client.Object ) (string , error ) {
727730 phaseMap := map [string ]any {
728- "name" : phase . GetName () ,
729- "objects" : phase . GetObjects () ,
731+ "name" : name ,
732+ "objects" : objects ,
730733 }
731734 data , err := json .Marshal (phaseMap )
732735 if err != nil {
733- return "" , fmt .Errorf ("marshaling phase %q: %w" , phase . GetName () , err )
736+ return "" , fmt .Errorf ("marshaling phase %q: %w" , name , err )
734737 }
735738 h := sha256 .Sum256 (data )
736739 return fmt .Sprintf ("sha256:%x" , h ), nil
737740}
738741
739742// verifyObservedPhases compares current per-phase digests against stored
740- // digests. Returns an error naming the first mismatched phase .
743+ // digests. Returns an error listing all mismatched phases .
741744func verifyObservedPhases (stored , current []ocv1.ObservedPhase ) error {
745+ if len (stored ) == 0 {
746+ return fmt .Errorf ("stored observedPhases is unexpectedly empty" )
747+ }
748+ if len (stored ) != len (current ) {
749+ return fmt .Errorf ("number of phases has changed (expected %d phases, got %d)" , len (stored ), len (current ))
750+ }
742751 storedMap := make (map [string ]string , len (stored ))
743752 for _ , s := range stored {
744753 storedMap [s .Name ] = s .Digest
745754 }
755+ var mismatches []string
746756 for _ , c := range current {
747757 if prev , ok := storedMap [c .Name ]; ok && prev != c .Digest {
748- return fmt .Errorf (
749- "resolved content of phase %q has changed (expected digest %s, got %s): " +
750- "a referenced object source may have been deleted and recreated with different content" ,
751- c .Name , prev , c .Digest )
758+ mismatches = append (mismatches , fmt .Sprintf (
759+ "phase %q (expected digest %s, got %s)" , c .Name , prev , c .Digest ))
752760 }
753761 }
762+ if len (mismatches ) > 0 {
763+ return fmt .Errorf (
764+ "resolved content of %d phase(s) has changed: %s; " +
765+ "a referenced object source may have been deleted and recreated with different content" ,
766+ len (mismatches ), strings .Join (mismatches , "; " ))
767+ }
754768 return nil
755769}
756770
@@ -778,6 +792,7 @@ func (c *ClusterObjectSetReconciler) verifyReferencedSecretsImmutable(ctx contex
778792 }
779793 }
780794
795+ var mutableSecrets []string
781796 for _ , ref := range refs {
782797 secret := & corev1.Secret {}
783798 key := client.ObjectKey {Name : ref .name , Namespace : ref .namespace }
@@ -791,9 +806,14 @@ func (c *ClusterObjectSetReconciler) verifyReferencedSecretsImmutable(ctx contex
791806 }
792807
793808 if secret .Immutable == nil || ! * secret .Immutable {
794- return fmt .Errorf ( "secret %s/%s is not immutable: referenced secrets must have immutable set to true " , ref .namespace , ref .name )
809+ mutableSecrets = append ( mutableSecrets , fmt .Sprintf ( " %s/%s" , ref .namespace , ref .name ) )
795810 }
796811 }
797812
813+ if len (mutableSecrets ) > 0 {
814+ return fmt .Errorf ("the following secrets are not immutable (referenced secrets must have immutable set to true): %s" ,
815+ strings .Join (mutableSecrets , ", " ))
816+ }
817+
798818 return nil
799819}
0 commit comments