diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 245d2e366..b5d292051 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -15,7 +15,7 @@ rules: - apiGroups: - catalogd.operatorframework.io resources: - - catalogsources + - catalogs verbs: - list - watch diff --git a/go.mod b/go.mod index af3b34a18..805cfbc5e 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/catalog_predicates.go b/internal/controllers/catalog_predicates.go deleted file mode 100644 index b35d10d5b..000000000 --- a/internal/controllers/catalog_predicates.go +++ /dev/null @@ -1,115 +0,0 @@ -package controllers - -import ( - "context" - "fmt" - - "github.com/go-logr/logr" - catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" - operatorv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/event" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" -) - -// Predicate for reconciling operators when available (i.e. ready) catalogsources on cluster change -type catalogReadyTransitionPredicate struct { - predicate.Funcs - catalogReady map[string]bool -} - -func newCatalogReadyTransitionPredicate() *catalogReadyTransitionPredicate { - return &catalogReadyTransitionPredicate{ - catalogReady: map[string]bool{}, - } -} - -func (c *catalogReadyTransitionPredicate) Create(e event.CreateEvent) bool { - fmt.Println("CreateEvent CatalogSource", e.Object.GetName()) - catalogReady, err := isCatalogReady(e.Object) - if err != nil { - fmt.Println(err) - return false - } - c.catalogReady[e.Object.GetName()] = catalogReady - return catalogReady -} - -func (c *catalogReadyTransitionPredicate) Update(e event.UpdateEvent) bool { - fmt.Println("UpdateEvent CatalogSource", e.ObjectOld.GetName(), e.ObjectNew.GetName()) - oldCatalogReady, err := isCatalogReady(e.ObjectOld) - if err != nil { - fmt.Println(err) - return false - } - - newCatalogReady, err := isCatalogReady(e.ObjectNew) - if err != nil { - fmt.Println(err) - return false - } - - c.catalogReady[e.ObjectNew.GetName()] = newCatalogReady - // TODO: determine if ready -> non-ready transition triggers reconcile with stale catalog contents - return oldCatalogReady != newCatalogReady -} - -func (c *catalogReadyTransitionPredicate) Delete(e event.DeleteEvent) bool { - fmt.Println("DeleteEvent CatalogSource", e.Object.GetName()) - delete(c.catalogReady, e.Object.GetName()) - return true -} - -func (c *catalogReadyTransitionPredicate) Generic(e event.GenericEvent) bool { - fmt.Println("GenericEvent CatalogSource", e.Object.GetName()) - catalogReady, err := isCatalogReady(e.Object) - if err != nil { - fmt.Println(err) - return false - } - predicateState := c.catalogReady[e.Object.GetName()] != catalogReady - c.catalogReady[e.Object.GetName()] = catalogReady - return predicateState -} - -func isCatalogReady(o client.Object) (bool, error) { - catalog, ok := o.(*catalogd.CatalogSource) - if !ok { - return false, fmt.Errorf("wrong object type: not a catalogsource: %+v", o) - } - if len(catalog.Status.Conditions) > 0 { - for _, cond := range catalog.Status.Conditions { - if cond.Type == catalogd.TypeReady && cond.Status == v1.ConditionTrue { - return true, nil - } - } - } - return false, nil -} - -// Generate reconcile requests for all operators affected by a catalog change -func operatorRequestsForCatalog(ctx context.Context, c client.Client, 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 := operatorv1alpha1.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/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index baf4e3e11..b96e8bb59 100644 --- a/internal/controllers/operator_controller.go +++ b/internal/controllers/operator_controller.go @@ -21,7 +21,6 @@ import ( "fmt" "github.com/go-logr/logr" - operatorv1 "github.com/operator-framework/api/pkg/operators/v1" 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" @@ -62,7 +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=catalogsources,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") @@ -293,7 +292,7 @@ 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.CatalogSource{}, mgr.GetCache()), + Watches(source.NewKindWithCache(&catalogd.Catalog{}, mgr.GetCache()), handler.EnqueueRequestsFromMapFunc(operatorRequestsForCatalog(context.TODO(), mgr.GetClient(), mgr.GetLogger()))). Owns(&rukpakv1alpha1.BundleDeployment{}). Complete(r) @@ -435,7 +434,7 @@ func setInstalledStatusConditionUnknown(conditions *[]metav1.Condition, message 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 := operatorv1.OperatorList{} + operators := operatorsv1alpha1.OperatorList{} err := c.List(ctx, &operators) if err != nil { logger.Error(err, "unable to enqueue operators for catalog reconcile") diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go index bfff23e1e..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,44 +103,25 @@ 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()) }) - AfterEach(func() { - err := c.Delete(ctx, operatorCatalog) - Expect(err).ToNot(HaveOccurred()) - err = c.Delete(ctx, operator) - Expect(err).ToNot(HaveOccurred()) - }) - }) - When("resolving for an unavailable operator package", func() { - BeforeEach(func() { - ctx = context.Background() - pkgName = "argocd-operator" - operatorName = fmt.Sprintf("operator-%s", rand.String(8)) - operator = &operatorv1alpha1.Operator{ - ObjectMeta: metav1.ObjectMeta{ - Name: operatorName, - }, - Spec: operatorv1alpha1.OperatorSpec{ - PackageName: pkgName, - }, - } - operatorCatalog = &catalogd.CatalogSource{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-catalog", - }, - Spec: catalogd.CatalogSourceSpec{ - // (TODO): Set up a local image registry, and build and store a test catalog in it - // to use in the test suite - Image: "quay.io/operatorhubio/catalog:latest", - }, - } - }) - 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()) @@ -143,7 +130,6 @@ var _ = Describe("Operator Install", func() { 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.ConditionFalse)) @@ -155,27 +141,60 @@ var _ = Describe("Operator Install", func() { err = c.Create(ctx, operatorCatalog) Expect(err).ToNot(HaveOccurred()) Eventually(func(g Gomega) { - err = c.Get(ctx, types.NamespacedName{Name: "test-catalog"}, operatorCatalog) + err = c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, operatorCatalog) g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(operatorCatalog.Status.Conditions)).To(Equal(1)) - g.Expect(operatorCatalog.Status.Conditions[0].Message).To(Equal("catalog contents have been unpacked and are available on cluster")) + 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 installing the package successfully") + By("eventually resolving the package successfully") Eventually(func(g Gomega) { - bd := rukpakv1alpha1.BundleDeployment{} - err = c.Get(ctx, types.NamespacedName{Name: operatorName}, &bd) + err = c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator) 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(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()) }) }) })