@@ -19,6 +19,7 @@ import (
1919 "k8s.io/utils/ptr"
2020 ctrl "sigs.k8s.io/controller-runtime"
2121 "sigs.k8s.io/controller-runtime/pkg/client"
22+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
2223
2324 ocv1 "github.com/operator-framework/operator-controller/api/v1"
2425 "github.com/operator-framework/operator-controller/internal/object-controller/scheme"
@@ -93,7 +94,7 @@ func TestStandaloneController(t *testing.T) {
9394 defer syncCancel ()
9495 require .True (t , mgr .GetCache ().WaitForCacheSync (syncCtx ), "manager cache did not synchronize" )
9596
96- for _ , name := range []string {"inline" , "secret-ref" } {
97+ for _ , name := range []string {"inline" , "secret-ref" , "mutable-secret-ref" } {
9798 t .Run (name , func (t * testing.T ) {
9899 ns := & corev1.Namespace {ObjectMeta : metav1.ObjectMeta {GenerateName : "standalone-" }}
99100 require .NoError (t , cl .Create (ctx , ns ))
@@ -103,12 +104,13 @@ func TestStandaloneController(t *testing.T) {
103104 "data" : map [string ]any {"hello" : "world" },
104105 }}
105106 obj := ocv1.ClusterObjectSetObject {Object : manifest }
106- if name == "secret-ref" {
107+ var secret * corev1.Secret
108+ if name != "inline" {
107109 data , err := json .Marshal (manifest .Object )
108110 require .NoError (t , err )
109- secret : = & corev1.Secret {
111+ secret = & corev1.Secret {
110112 ObjectMeta : metav1.ObjectMeta {Name : "content" , Namespace : ns .Name },
111- Immutable : ptr .To (true ), Data : map [string ][]byte {"object" : data },
113+ Immutable : ptr .To (name != "mutable-secret-ref" ), Data : map [string ][]byte {"object" : data },
112114 }
113115 require .NoError (t , cl .Create (ctx , secret ))
114116 obj = ocv1.ClusterObjectSetObject {Ref : ocv1.ObjectSourceRef {Name : secret .Name , Namespace : secret .Namespace , Key : "object" }}
@@ -122,6 +124,39 @@ func TestStandaloneController(t *testing.T) {
122124 },
123125 }
124126 require .NoError (t , cl .Create (ctx , cos ))
127+ if secret != nil {
128+ require .NoError (t , controllerutil .SetControllerReference (cos , secret , scheme .Scheme ))
129+ require .NoError (t , cl .Update (ctx , secret ))
130+ }
131+ rolloutTimeout := time .Minute
132+ if name == "mutable-secret-ref" {
133+ require .EventuallyWithT (t , func (collect * assert.CollectT ) {
134+ if ! assert .NoError (collect , cl .Get (ctx , client .ObjectKeyFromObject (cos ), cos )) {
135+ return
136+ }
137+ condition := meta .FindStatusCondition (cos .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
138+ if assert .NotNil (collect , condition ) {
139+ assert .Equal (collect , ocv1 .ClusterObjectSetReasonBlocked , condition .Reason )
140+ assert .Contains (collect , condition .Message , "not immutable" )
141+ }
142+ }, 5 * time .Second , 100 * time .Millisecond )
143+
144+ // Let status-triggered reconciliations settle before changing only
145+ // the Secret. Recovery must precede the 10-second polling retry.
146+ lastVersion , unchangedSince := cos .ResourceVersion , time .Now ()
147+ require .Eventually (t , func () bool {
148+ if err := cl .Get (ctx , client .ObjectKeyFromObject (cos ), cos ); err != nil {
149+ return false
150+ }
151+ if cos .ResourceVersion != lastVersion {
152+ lastVersion , unchangedSince = cos .ResourceVersion , time .Now ()
153+ }
154+ return time .Since (unchangedSince ) >= time .Second
155+ }, 3 * time .Second , 100 * time .Millisecond )
156+ secret .Immutable = ptr .To (true )
157+ require .NoError (t , cl .Update (ctx , secret ))
158+ rolloutTimeout = 5 * time .Second
159+ }
125160 require .EventuallyWithT (t , func (collect * assert.CollectT ) {
126161 if ! assert .NoError (collect , cl .Get (ctx , client .ObjectKeyFromObject (cos ), cos )) {
127162 return
@@ -136,13 +171,52 @@ func TestStandaloneController(t *testing.T) {
136171 if assert .NotNil (collect , progressing ) {
137172 assert .Equal (collect , ocv1 .ReasonSucceeded , progressing .Reason )
138173 }
139- }, time . Minute , 100 * time .Millisecond )
174+ }, rolloutTimeout , 100 * time .Millisecond )
140175 cm := & corev1.ConfigMap {}
141176 require .NoError (t , cl .Get (ctx , client.ObjectKey {Name : name , Namespace : ns .Name }, cm ))
142177 require .Equal (t , "world" , cm .Data ["hello" ])
143178 require .NotNil (t , metav1 .GetControllerOf (cm ))
144179 require .Equal (t , cos .UID , metav1 .GetControllerOf (cm ).UID )
145180
181+ if secret != nil {
182+ // A completed COS does not poll. Replacing an owned source Secret
183+ // must trigger content verification, and restoring it must unblock
184+ // reconciliation without changing the COS or its managed objects.
185+ original := secret .DeepCopy ()
186+ require .NoError (t , cl .Delete (ctx , secret ))
187+ secret .ResourceVersion = ""
188+ secret .UID = ""
189+ changed := manifest .DeepCopy ()
190+ changed .Object ["data" ] = map [string ]any {"hello" : "changed" }
191+ secret .Data ["object" ], err = json .Marshal (changed .Object )
192+ require .NoError (t , err )
193+ require .NoError (t , cl .Create (ctx , secret ))
194+ require .EventuallyWithT (t , func (collect * assert.CollectT ) {
195+ if ! assert .NoError (collect , cl .Get (ctx , client .ObjectKeyFromObject (cos ), cos )) {
196+ return
197+ }
198+ condition := meta .FindStatusCondition (cos .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
199+ if assert .NotNil (collect , condition ) {
200+ assert .Equal (collect , ocv1 .ClusterObjectSetReasonBlocked , condition .Reason )
201+ assert .Contains (collect , condition .Message , "resolved content of 1 phase(s) has changed" )
202+ }
203+ }, 30 * time .Second , 100 * time .Millisecond )
204+
205+ require .NoError (t , cl .Delete (ctx , secret ))
206+ original .ResourceVersion = ""
207+ original .UID = ""
208+ require .NoError (t , cl .Create (ctx , original ))
209+ require .EventuallyWithT (t , func (collect * assert.CollectT ) {
210+ if ! assert .NoError (collect , cl .Get (ctx , client .ObjectKeyFromObject (cos ), cos )) {
211+ return
212+ }
213+ condition := meta .FindStatusCondition (cos .Status .Conditions , ocv1 .ClusterObjectSetTypeProgressing )
214+ if assert .NotNil (collect , condition ) {
215+ assert .Equal (collect , ocv1 .ReasonSucceeded , condition .Reason )
216+ }
217+ }, 30 * time .Second , 100 * time .Millisecond )
218+ }
219+
146220 // Observe managed-object changes without updating the ClusterObjectSet.
147221 originalUID := cm .UID
148222 require .NoError (t , cl .Delete (ctx , cm ))
0 commit comments