diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 63e792ef4..b5d292051 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -12,6 +12,13 @@ rules: verbs: - list - watch +- apiGroups: + - catalogd.operatorframework.io + resources: + - catalogs + verbs: + - list + - watch - apiGroups: - catalogd.operatorframework.io resources: diff --git a/go.mod b/go.mod index 61b8e2986..634744d46 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/blang/semver/v4 v4.0.0 + github.com/go-logr/logr v1.2.3 github.com/onsi/ginkgo/v2 v2.8.3 github.com/onsi/gomega v1.27.1 github.com/operator-framework/catalogd v0.2.0 @@ -26,7 +27,6 @@ require ( github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-air/gini v1.0.4 // indirect - github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect diff --git a/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index 0c1f85f1c..b96e8bb59 100644 --- a/internal/controllers/operator_controller.go +++ b/internal/controllers/operator_controller.go @@ -20,6 +20,8 @@ import ( "context" "fmt" + "github.com/go-logr/logr" + catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" "github.com/operator-framework/deppy/pkg/deppy/solver" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "k8s.io/apimachinery/pkg/api/equality" @@ -32,11 +34,13 @@ import ( "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/operator-framework/operator-controller/internal/controllers/validators" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/controllers/validators" "github.com/operator-framework/operator-controller/internal/resolution" "github.com/operator-framework/operator-controller/internal/resolution/variable_sources/bundles_and_dependencies" "github.com/operator-framework/operator-controller/internal/resolution/variable_sources/entity" @@ -57,6 +61,7 @@ type OperatorReconciler struct { //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=bundlemetadata,verbs=list;watch //+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=packages,verbs=list;watch +//+kubebuilder:rbac:groups=catalogd.operatorframework.io,resources=catalogs,verbs=list;watch func (r *OperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { l := log.FromContext(ctx).WithName("operator-controller") @@ -287,6 +292,8 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha func (r *OperatorReconciler) SetupWithManager(mgr ctrl.Manager) error { err := ctrl.NewControllerManagedBy(mgr). For(&operatorsv1alpha1.Operator{}). + Watches(source.NewKindWithCache(&catalogd.Catalog{}, mgr.GetCache()), + handler.EnqueueRequestsFromMapFunc(operatorRequestsForCatalog(context.TODO(), mgr.GetClient(), mgr.GetLogger()))). Owns(&rukpakv1alpha1.BundleDeployment{}). Complete(r) @@ -422,3 +429,26 @@ func setInstalledStatusConditionUnknown(conditions *[]metav1.Condition, message ObservedGeneration: generation, }) } + +// Generate reconcile requests for all operators affected by a catalog change +func operatorRequestsForCatalog(ctx context.Context, c client.Reader, logger logr.Logger) handler.MapFunc { + return func(object client.Object) []reconcile.Request { + // no way of associating an operator to a catalog so create reconcile requests for everything + operators := operatorsv1alpha1.OperatorList{} + err := c.List(ctx, &operators) + if err != nil { + logger.Error(err, "unable to enqueue operators for catalog reconcile") + return nil + } + var requests []reconcile.Request + for _, op := range operators.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: op.GetNamespace(), + Name: op.GetName(), + }, + }) + } + return requests + } +} diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go index 8d8a7b9d4..0448c268e 100644 --- a/test/e2e/install_test.go +++ b/test/e2e/install_test.go @@ -10,6 +10,7 @@ import ( catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" operatorv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -52,30 +53,35 @@ var _ = Describe("Operator Install", func() { Image: &catalogd.ImageSource{ // (TODO): Set up a local image registry, and build and store a test catalog in it // to use in the test suite - Ref: "quay.io/operatorhubio/catalog:latest", + Ref: "quay.io/olmtest/e2e-index:single-package-fbc", //generated from: "quay.io/operatorhubio/catalog:latest", }, }, }, } + }) + It("resolves the specified package with correct bundle path", func() { err := c.Create(ctx, operatorCatalog) Expect(err).ToNot(HaveOccurred()) Eventually(func(g Gomega) { err = c.Get(ctx, types.NamespacedName{Name: "test-catalog"}, operatorCatalog) g.Expect(err).ToNot(HaveOccurred()) g.Expect(len(operatorCatalog.Status.Conditions)).To(Equal(1)) - g.Expect(operatorCatalog.Status.Conditions[0].Message).To(ContainSubstring("successfully unpacked the catalog image")) + cond := apimeta.FindStatusCondition(operatorCatalog.Status.Conditions, catalogd.TypeUnpacked) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(catalogd.ReasonUnpackSuccessful)) + g.Expect(cond.Message).To(ContainSubstring("successfully unpacked the catalog image")) }).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(Succeed()) - }) - It("resolves the specified package with correct bundle path", func() { + By("creating the Operator resource") - err := c.Create(ctx, operator) + err = c.Create(ctx, operator) Expect(err).ToNot(HaveOccurred()) By("eventually reporting a successful resolution and bundle path") Eventually(func(g Gomega) { err = c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(operator.Status.Conditions)).To(Equal(2)) + cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved) g.Expect(cond).ToNot(BeNil()) g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) @@ -97,17 +103,98 @@ var _ = Describe("Operator Install", func() { bd := rukpakv1alpha1.BundleDeployment{} err = c.Get(ctx, types.NamespacedName{Name: operatorName}, &bd) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(bd.Status.Conditions)).To(Equal(2)) - g.Expect(bd.Status.Conditions[0].Reason).To(Equal("UnpackSuccessful")) - g.Expect(bd.Status.Conditions[1].Reason).To(Equal("InstallationSucceeded")) + + cond = apimeta.FindStatusCondition(bd.Status.Conditions, rukpakv1alpha1.TypeHasValidBundle) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(rukpakv1alpha1.ReasonUnpackSuccessful)) + + cond = apimeta.FindStatusCondition(bd.Status.Conditions, rukpakv1alpha1.TypeInstalled) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(rukpakv1alpha1.ReasonInstallationSucceeded)) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + }) + It("resolves again when a new catalog is available", func() { + Eventually(func(g Gomega) { + // target package should not be present on cluster + err := c.Get(ctx, types.NamespacedName{Name: pkgName}, &catalogd.Package{}) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(Succeed()) + + By("creating the Operator resource") + err := c.Create(ctx, operator) + Expect(err).ToNot(HaveOccurred()) + + By("failing to find Operator during resolution") + Eventually(func(g Gomega) { + err = c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator) + g.Expect(err).ToNot(HaveOccurred()) + cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonResolutionFailed)) + g.Expect(cond.Message).To(Equal(fmt.Sprintf("package '%s' not found", pkgName))) }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + By("creating an Operator catalog with the desired package") + err = c.Create(ctx, operatorCatalog) + Expect(err).ToNot(HaveOccurred()) + Eventually(func(g Gomega) { + err = c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, operatorCatalog) + g.Expect(err).ToNot(HaveOccurred()) + cond := apimeta.FindStatusCondition(operatorCatalog.Status.Conditions, catalogd.TypeUnpacked) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(catalogd.ReasonUnpackSuccessful)) + }).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(Succeed()) + + By("eventually resolving the package successfully") + Eventually(func(g Gomega) { + err = c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator) + g.Expect(err).ToNot(HaveOccurred()) + cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess)) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) }) AfterEach(func() { - err := c.Delete(ctx, operatorCatalog) + err := c.Delete(ctx, operator) + Expect(err).ToNot(HaveOccurred()) + Eventually(func(g Gomega) { + err = c.Get(ctx, types.NamespacedName{Name: operatorName}, &operatorv1alpha1.Operator{}) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + + err = c.Delete(ctx, operatorCatalog) + Expect(err).ToNot(HaveOccurred()) + Eventually(func(g Gomega) { + err = c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, &catalogd.Catalog{}) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + + // speed up delete without waiting for gc + err = c.DeleteAllOf(ctx, &catalogd.BundleMetadata{}) Expect(err).ToNot(HaveOccurred()) - err = c.Delete(ctx, operator) + err = c.DeleteAllOf(ctx, &catalogd.Package{}) Expect(err).ToNot(HaveOccurred()) + + Eventually(func(g Gomega) { + // ensure resource cleanup + packages := &catalogd.PackageList{} + err = c.List(ctx, packages) + Expect(err).To(BeNil()) + Expect(packages.Items).To(BeEmpty()) + + bmd := &catalogd.BundleMetadataList{} + err = c.List(ctx, bmd) + Expect(err).To(BeNil()) + Expect(bmd.Items).To(BeEmpty()) + + err = c.Get(ctx, types.NamespacedName{Name: operatorName}, &rukpakv1alpha1.BundleDeployment{}) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(Succeed()) }) }) })