Skip to content

Commit 9125118

Browse files
authored
Update Installed status condition handling (#1314)
Signed-off-by: Tayler Geiger <[email protected]>
1 parent a4a1c6c commit 9125118

File tree

4 files changed

+224
-16
lines changed

4 files changed

+224
-16
lines changed

cmd/manager/main.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import (
5454
"github.com/operator-framework/operator-controller/internal/contentmanager"
5555
"github.com/operator-framework/operator-controller/internal/controllers"
5656
"github.com/operator-framework/operator-controller/internal/features"
57+
"github.com/operator-framework/operator-controller/internal/finalizers"
5758
"github.com/operator-framework/operator-controller/internal/httputil"
5859
"github.com/operator-framework/operator-controller/internal/labels"
5960
"github.com/operator-framework/operator-controller/internal/resolve"
@@ -207,7 +208,7 @@ func main() {
207208
}
208209

209210
clusterExtensionFinalizers := crfinalizer.NewFinalizers()
210-
if err := clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupUnpackCacheFinalizer, finalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
211+
if err := clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupUnpackCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
211212
return crfinalizer.Result{}, unpacker.Cleanup(ctx, &source.BundleSource{Name: obj.GetName()})
212213
})); err != nil {
213214
setupLog.Error(err, "unable to register finalizer", "finalizerKey", controllers.ClusterExtensionCleanupUnpackCacheFinalizer)
@@ -258,7 +259,7 @@ func main() {
258259
}
259260

260261
cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())
261-
err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
262+
err = clusterExtensionFinalizers.Register(controllers.ClusterExtensionCleanupContentManagerCacheFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
262263
ext := obj.(*ocv1alpha1.ClusterExtension)
263264
err := cm.Delete(ext)
264265
return crfinalizer.Result{}, err
@@ -308,12 +309,6 @@ func main() {
308309
}
309310
}
310311

311-
type finalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error)
312-
313-
func (f finalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
314-
return f(ctx, obj)
315-
}
316-
317312
func authFilePathIfPresent(logger logr.Logger) string {
318313
_, err := os.Stat(authFilePath)
319314
if os.IsNotExist(err) {

internal/controllers/clusterextension_controller.go

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,9 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp
192192
l.Info("handling finalizers")
193193
finalizeResult, err := r.Finalizers.Finalize(ctx, ext)
194194
if err != nil {
195-
// TODO: For now, this error handling follows the pattern of other error handling.
196-
// Namely: zero just about everything out, throw our hands up, and return an error.
197-
// This is not ideal, and we should consider a more nuanced approach that resolves
198-
// as much status as possible before returning, or at least keeps previous state if
199-
// it is properly labeled with its observed generation.
200-
setInstallStatus(ext, nil)
201195
setResolutionStatus(ext, nil)
202196
setStatusProgressing(ext, err)
203-
ensureAllConditionsWithReason(ext, ocv1alpha1.ReasonFailed, err.Error())
197+
204198
return ctrl.Result{}, err
205199
}
206200
if finalizeResult.Updated || finalizeResult.StatusUpdated {
@@ -297,8 +291,11 @@ func (r *ClusterExtensionReconciler) reconcile(ctx context.Context, ext *ocv1alp
297291
// The only way to eventually recover from permission errors is to keep retrying).
298292
managedObjs, _, err := r.Applier.Apply(ctx, unpackResult.Bundle, ext, objLbls, storeLbls)
299293
if err != nil {
300-
setInstalledStatusConditionFailed(ext, err.Error())
301294
setStatusProgressing(ext, wrapErrorWithResolutionInfo(resolvedBundleMetadata, err))
295+
// If bundle is not already installed, set Installed status condition to False
296+
if installedBundle == nil {
297+
setInstalledStatusConditionFailed(ext, err.Error())
298+
}
302299
return ctrl.Result{}, err
303300
}
304301

internal/controllers/clusterextension_controller_test.go

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import (
1818
"k8s.io/apimachinery/pkg/util/rand"
1919
ctrl "sigs.k8s.io/controller-runtime"
2020
"sigs.k8s.io/controller-runtime/pkg/client"
21+
crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer"
2122
"sigs.k8s.io/controller-runtime/pkg/reconcile"
2223

2324
"github.com/operator-framework/operator-registry/alpha/declcfg"
2425

2526
ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
2627
"github.com/operator-framework/operator-controller/internal/conditionsets"
2728
"github.com/operator-framework/operator-controller/internal/controllers"
29+
"github.com/operator-framework/operator-controller/internal/finalizers"
2830
"github.com/operator-framework/operator-controller/internal/resolve"
2931
"github.com/operator-framework/operator-controller/internal/rukpak/source"
3032
)
@@ -336,6 +338,105 @@ func TestClusterExtensionResolutionAndUnpackSuccessfulApplierFails(t *testing.T)
336338
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
337339
}
338340

341+
func TestClusterExtensionApplierFailsWithBundleInstalled(t *testing.T) {
342+
cl, reconciler := newClientAndReconciler(t)
343+
reconciler.Unpacker = &MockUnpacker{
344+
result: &source.Result{
345+
State: source.StateUnpacked,
346+
Bundle: fstest.MapFS{},
347+
},
348+
}
349+
350+
ctx := context.Background()
351+
extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))}
352+
353+
t.Log("When the cluster extension specifies a channel with version that exist")
354+
t.Log("By initializing cluster state")
355+
pkgName := "prometheus"
356+
pkgVer := "1.0.0"
357+
pkgChan := "beta"
358+
namespace := fmt.Sprintf("test-ns-%s", rand.String(8))
359+
serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8))
360+
361+
clusterExtension := &ocv1alpha1.ClusterExtension{
362+
ObjectMeta: metav1.ObjectMeta{Name: extKey.Name},
363+
Spec: ocv1alpha1.ClusterExtensionSpec{
364+
Source: ocv1alpha1.SourceConfig{
365+
SourceType: "Catalog",
366+
Catalog: &ocv1alpha1.CatalogSource{
367+
PackageName: pkgName,
368+
Version: pkgVer,
369+
Channels: []string{pkgChan},
370+
},
371+
},
372+
Install: ocv1alpha1.ClusterExtensionInstallConfig{
373+
Namespace: namespace,
374+
ServiceAccount: ocv1alpha1.ServiceAccountReference{
375+
Name: serviceAccount,
376+
},
377+
},
378+
},
379+
}
380+
err := cl.Create(ctx, clusterExtension)
381+
require.NoError(t, err)
382+
383+
t.Log("It sets resolution success status")
384+
t.Log("By running reconcile")
385+
reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) {
386+
v := bsemver.MustParse("1.0.0")
387+
return &declcfg.Bundle{
388+
Name: "prometheus.v1.0.0",
389+
Package: "prometheus",
390+
Image: "quay.io/operatorhubio/[email protected]",
391+
}, &v, nil, nil
392+
})
393+
394+
reconciler.Manager = &MockManagedContentCacheManager{
395+
cache: &MockManagedContentCache{},
396+
}
397+
reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{
398+
bundle: &ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"},
399+
}
400+
reconciler.Applier = &MockApplier{
401+
objs: []client.Object{},
402+
}
403+
404+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
405+
require.Equal(t, ctrl.Result{}, res)
406+
require.NoError(t, err)
407+
408+
reconciler.Applier = &MockApplier{
409+
err: errors.New("apply failure"),
410+
}
411+
412+
res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
413+
require.Equal(t, ctrl.Result{}, res)
414+
require.Error(t, err)
415+
416+
t.Log("By fetching updated cluster extension after reconcile")
417+
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
418+
419+
t.Log("By checking the status fields")
420+
expectedBundleMetadata := ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}
421+
require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Resolution.Bundle)
422+
require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Install.Bundle)
423+
424+
t.Log("By checking the expected installed conditions")
425+
installedCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled)
426+
require.NotNil(t, installedCond)
427+
require.Equal(t, metav1.ConditionTrue, installedCond.Status)
428+
require.Equal(t, ocv1alpha1.ReasonSuccess, installedCond.Reason)
429+
430+
t.Log("By checking the expected progressing conditions")
431+
progressingCond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing)
432+
require.NotNil(t, progressingCond)
433+
require.Equal(t, metav1.ConditionTrue, progressingCond.Status)
434+
require.Equal(t, ocv1alpha1.ReasonRetrying, progressingCond.Reason)
435+
require.Contains(t, progressingCond.Message, fmt.Sprintf("for resolved bundle %q with version %q", expectedBundleMetadata.Name, expectedBundleMetadata.Version))
436+
437+
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
438+
}
439+
339440
func TestClusterExtensionManagerFailed(t *testing.T) {
340441
cl, reconciler := newClientAndReconciler(t)
341442
reconciler.Unpacker = &MockUnpacker{
@@ -591,6 +692,107 @@ func TestClusterExtensionInstallationSucceeds(t *testing.T) {
591692
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
592693
}
593694

695+
func TestClusterExtensionDeleteFinalizerFails(t *testing.T) {
696+
cl, reconciler := newClientAndReconciler(t)
697+
reconciler.Unpacker = &MockUnpacker{
698+
result: &source.Result{
699+
State: source.StateUnpacked,
700+
Bundle: fstest.MapFS{},
701+
},
702+
}
703+
704+
ctx := context.Background()
705+
extKey := types.NamespacedName{Name: fmt.Sprintf("cluster-extension-test-%s", rand.String(8))}
706+
707+
t.Log("When the cluster extension specifies a channel with version that exist")
708+
t.Log("By initializing cluster state")
709+
pkgName := "prometheus"
710+
pkgVer := "1.0.0"
711+
pkgChan := "beta"
712+
namespace := fmt.Sprintf("test-ns-%s", rand.String(8))
713+
serviceAccount := fmt.Sprintf("test-sa-%s", rand.String(8))
714+
715+
clusterExtension := &ocv1alpha1.ClusterExtension{
716+
ObjectMeta: metav1.ObjectMeta{Name: extKey.Name},
717+
Spec: ocv1alpha1.ClusterExtensionSpec{
718+
Source: ocv1alpha1.SourceConfig{
719+
SourceType: "Catalog",
720+
Catalog: &ocv1alpha1.CatalogSource{
721+
PackageName: pkgName,
722+
Version: pkgVer,
723+
Channels: []string{pkgChan},
724+
},
725+
},
726+
Install: ocv1alpha1.ClusterExtensionInstallConfig{
727+
Namespace: namespace,
728+
ServiceAccount: ocv1alpha1.ServiceAccountReference{
729+
Name: serviceAccount,
730+
},
731+
},
732+
},
733+
}
734+
err := cl.Create(ctx, clusterExtension)
735+
require.NoError(t, err)
736+
t.Log("It sets resolution success status")
737+
t.Log("By running reconcile")
738+
reconciler.Resolver = resolve.Func(func(_ context.Context, _ *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) {
739+
v := bsemver.MustParse("1.0.0")
740+
return &declcfg.Bundle{
741+
Name: "prometheus.v1.0.0",
742+
Package: "prometheus",
743+
Image: "quay.io/operatorhubio/[email protected]",
744+
}, &v, nil, nil
745+
})
746+
fakeFinalizer := "fake.testfinalizer.io"
747+
finalizersMessage := "still have finalizers"
748+
reconciler.Applier = &MockApplier{
749+
objs: []client.Object{},
750+
}
751+
reconciler.Manager = &MockManagedContentCacheManager{
752+
cache: &MockManagedContentCache{},
753+
}
754+
reconciler.InstalledBundleGetter = &MockInstalledBundleGetter{
755+
bundle: &ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"},
756+
}
757+
err = reconciler.Finalizers.Register(fakeFinalizer, finalizers.FinalizerFunc(func(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
758+
return crfinalizer.Result{}, errors.New(finalizersMessage)
759+
}))
760+
761+
require.NoError(t, err)
762+
763+
// Reconcile twice to simulate installing the ClusterExtension and loading in the finalizers
764+
res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
765+
require.Equal(t, ctrl.Result{}, res)
766+
require.NoError(t, err)
767+
res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
768+
require.Equal(t, ctrl.Result{}, res)
769+
require.NoError(t, err)
770+
771+
t.Log("By fetching updated cluster extension after first reconcile")
772+
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
773+
cond := apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled)
774+
expectedBundleMetadata := ocv1alpha1.BundleMetadata{Name: "prometheus.v1.0.0", Version: "1.0.0"}
775+
require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Install.Bundle)
776+
require.NotNil(t, cond)
777+
require.Equal(t, metav1.ConditionTrue, cond.Status)
778+
779+
require.NoError(t, cl.DeleteAllOf(ctx, &ocv1alpha1.ClusterExtension{}))
780+
res, err = reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: extKey})
781+
require.Error(t, err, res)
782+
783+
t.Log("By fetching updated cluster extension after second reconcile")
784+
require.NoError(t, cl.Get(ctx, extKey, clusterExtension))
785+
cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeInstalled)
786+
require.Equal(t, expectedBundleMetadata, clusterExtension.Status.Install.Bundle)
787+
require.NotNil(t, cond)
788+
require.Equal(t, metav1.ConditionTrue, cond.Status)
789+
require.Equal(t, fakeFinalizer, clusterExtension.Finalizers[0])
790+
cond = apimeta.FindStatusCondition(clusterExtension.Status.Conditions, ocv1alpha1.TypeProgressing)
791+
require.NotNil(t, cond)
792+
require.Equal(t, metav1.ConditionTrue, cond.Status)
793+
require.Contains(t, cond.Message, finalizersMessage)
794+
}
795+
594796
func verifyInvariants(ctx context.Context, t *testing.T, c client.Client, ext *ocv1alpha1.ClusterExtension) {
595797
key := client.ObjectKeyFromObject(ext)
596798
require.NoError(t, c.Get(ctx, key, ext))

internal/finalizers/finalizers.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package finalizers
2+
3+
import (
4+
"context"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
crfinalizer "sigs.k8s.io/controller-runtime/pkg/finalizer"
8+
)
9+
10+
type FinalizerFunc func(ctx context.Context, obj client.Object) (crfinalizer.Result, error)
11+
12+
func (f FinalizerFunc) Finalize(ctx context.Context, obj client.Object) (crfinalizer.Result, error) {
13+
return f(ctx, obj)
14+
}

0 commit comments

Comments
 (0)