@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "errors"
2222
23+ "github.com/go-logr/logr"
2324 . "github.com/onsi/ginkgo/v2"
2425 . "github.com/onsi/gomega"
2526
@@ -31,7 +32,9 @@ import (
3132 "sigs.k8s.io/controller-runtime/pkg/client"
3233 "sigs.k8s.io/controller-runtime/pkg/client/fake"
3334 "sigs.k8s.io/controller-runtime/pkg/client/interceptor"
35+ "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3436
37+ pkgStatus "github.com/operator-framework/helm-operator-plugins/pkg/internal/status"
3538 "github.com/operator-framework/helm-operator-plugins/pkg/reconciler/internal/conditions"
3639)
3740
@@ -41,6 +44,10 @@ const (
4144 replicasStatus = int64 (5 )
4245)
4346
47+ var (
48+ transientError = errors .New ("transient error" )
49+ )
50+
4451var _ = Describe ("Updater" , func () {
4552 var (
4653 cl client.Client
@@ -51,7 +58,7 @@ var _ = Describe("Updater", func() {
5158
5259 JustBeforeEach (func () {
5360 cl = fake .NewClientBuilder ().WithInterceptorFuncs (interceptorFuncs ).Build ()
54- u = New (cl )
61+ u = New (cl , logr . Discard () )
5562 obj = & unstructured.Unstructured {Object : map [string ]interface {}{
5663 "apiVersion" : "apps/v1" ,
5764 "kind" : "Deployment" ,
@@ -85,7 +92,7 @@ var _ = Describe("Updater", func() {
8592 interceptorFuncs .SubResourceUpdate = func (ctx context.Context , interceptorClient client.Client , subResourceName string , obj client.Object , opts ... client.SubResourceUpdateOption ) error {
8693 updateCallCount ++
8794 if updateCallCount == 1 {
88- return errors . New ( "transient error" )
95+ return transientError
8996 }
9097 return interceptorClient .SubResource (subResourceName ).Update (ctx , obj , opts ... )
9198 }
@@ -177,9 +184,85 @@ var _ = Describe("Updater", func() {
177184 Expect (found ).To (BeTrue ())
178185 Expect (err ).To (Succeed ())
179186 })
187+
188+ It ("should add a finalizer" , func () {
189+ u .Update (func (u * unstructured.Unstructured ) bool {
190+ return controllerutil .AddFinalizer (u , testFinalizer )
191+ })
192+ Expect (u .Apply (context .TODO (), obj )).To (Succeed ())
193+ Expect (cl .Get (context .TODO (), types.NamespacedName {Namespace : "testNamespace" , Name : "testDeployment" }, obj )).To (Succeed ())
194+ Expect (obj .GetFinalizers ()).To (ContainElement (testFinalizer ))
195+ })
196+
197+ It ("should remove a finalizer" , func () {
198+ obj .SetFinalizers ([]string {testFinalizer })
199+ Expect (cl .Update (context .TODO (), obj )).To (Succeed ())
200+
201+ u .Update (func (u * unstructured.Unstructured ) bool {
202+ return controllerutil .RemoveFinalizer (u , testFinalizer )
203+ })
204+ Expect (u .Apply (context .TODO (), obj )).To (Succeed ())
205+ Expect (cl .Get (context .TODO (), types.NamespacedName {Namespace : "testNamespace" , Name : "testDeployment" }, obj )).To (Succeed ())
206+ Expect (obj .GetFinalizers ()).ToNot (ContainElement (testFinalizer ))
207+ })
208+
209+ It ("should preserve unknown status conditions" , func () {
210+ // Add external status condition on cluster.
211+ clusterObj := obj .DeepCopy ()
212+ unknownCondition := map [string ]interface {}{
213+ "type" : "UnknownCondition" ,
214+ "status" : string (corev1 .ConditionTrue ),
215+ "reason" : "ExternallyManaged" ,
216+ }
217+ Expect (unstructured .SetNestedSlice (clusterObj .Object , []interface {}{unknownCondition }, "status" , "conditions" )).To (Succeed ())
218+ Expect (retryOnTransientError (func () error {
219+ return cl .Status ().Update (context .TODO (), clusterObj )
220+ })).ToNot (HaveOccurred ())
221+ // Add status condition using updater.
222+ u .UpdateStatus (EnsureCondition (conditions .Deployed (corev1 .ConditionTrue , "" , "" )))
223+ u .EnableAggressiveConflictResolution ()
224+ Expect (u .Apply (context .TODO (), obj )).To (Succeed ())
225+ // Retrieve object from cluster and extract status conditions.
226+ Expect (cl .Get (context .TODO (), types.NamespacedName {Namespace : "testNamespace" , Name : "testDeployment" }, obj )).To (Succeed ())
227+ objConditionsSlice , _ , err := unstructured .NestedSlice (obj .Object , "status" , "conditions" )
228+ Expect (err ).ToNot (HaveOccurred ())
229+ objConditions , err := pkgStatus .FromUnstructured (objConditionsSlice )
230+ Expect (err ).ToNot (HaveOccurred ())
231+ // Verify both status conditions are present.
232+ Expect (objConditions .IsTrueFor (pkgStatus .ConditionType ("UnknownCondition" ))).To (BeTrue ())
233+ Expect (objConditions .IsTrueFor (pkgStatus .ConditionType ("Deployed" ))).To (BeTrue ())
234+ })
235+
236+ It ("should fail on conflict without aggressive resolution" , func () {
237+ // Add external status condition on cluster.
238+ clusterObj := obj .DeepCopy ()
239+ unknownCondition := map [string ]interface {}{
240+ "type" : "UnknownCondition" ,
241+ "status" : string (corev1 .ConditionTrue ),
242+ "reason" : "ExternallyManaged" ,
243+ }
244+ Expect (unstructured .SetNestedSlice (clusterObj .Object , []interface {}{unknownCondition }, "status" , "conditions" )).To (Succeed ())
245+ Expect (retryOnTransientError (func () error {
246+ return cl .Status ().Update (context .TODO (), clusterObj )
247+ })).ToNot (HaveOccurred ())
248+ Expect (cl .Status ().Update (context .TODO (), clusterObj )).To (Succeed ())
249+ // Add status condition using updater.
250+ u .UpdateStatus (EnsureCondition (conditions .Deployed (corev1 .ConditionTrue , "" , "" )))
251+ err := u .Apply (context .TODO (), obj )
252+ // Verify conflict error is returned.
253+ Expect (apierrors .IsConflict (err )).To (BeTrue ())
254+ })
180255 })
181256})
182257
258+ func retryOnTransientError (f func () error ) error {
259+ err := f ()
260+ if errors .Is (err , transientError ) {
261+ err = f ()
262+ }
263+ return err
264+ }
265+
183266var _ = Describe ("RemoveFinalizer" , func () {
184267 var obj * unstructured.Unstructured
185268
@@ -325,4 +408,19 @@ var _ = Describe("statusFor", func() {
325408 obj .Object ["status" ] = "hello"
326409 Expect (statusFor (obj )).To (Equal (& helmAppStatus {}))
327410 })
411+
412+ It ("should handle unknown status conditions" , func () {
413+ uSt := map [string ]interface {}{
414+ "conditions" : []interface {}{
415+ map [string ]interface {}{
416+ "type" : "UnknownCondition" ,
417+ "status" : string (corev1 .ConditionTrue ),
418+ },
419+ },
420+ }
421+ obj .Object ["status" ] = uSt
422+ status := statusFor (obj )
423+ Expect (status ).ToNot (BeNil ())
424+ Expect (status .Conditions .IsTrueFor (pkgStatus .ConditionType ("UnknownCondition" ))).To (BeTrue ())
425+ })
328426})
0 commit comments