From 0ed3ede4212223509f7ebeaf5ed9faefb23f06b3 Mon Sep 17 00:00:00 2001 From: Ankita Thomas Date: Thu, 1 Jun 2023 11:58:12 -0700 Subject: [PATCH 1/3] refactor catalogsource e2e test to unit test Signed-off-by: Ankita Thomas --- go.mod | 1 + .../controllers/operator_controller_test.go | 99 +++++++++++++++++++ internal/controllers/suite_test.go | 3 + 3 files changed, 103 insertions(+) diff --git a/go.mod b/go.mod index ed4937988a..d2554dd1ee 100644 --- a/go.mod +++ b/go.mod @@ -48,6 +48,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/operator-framework/api v0.17.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect diff --git a/internal/controllers/operator_controller_test.go b/internal/controllers/operator_controller_test.go index 8e75327b73..84a8d41c30 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -3,9 +3,13 @@ package controllers_test import ( "context" "fmt" + "strings" + "time" + "github.com/go-logr/logr/funcr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/input" "github.com/operator-framework/deppy/pkg/deppy/solver" @@ -18,6 +22,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/log" operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/conditionsets" @@ -1057,6 +1062,94 @@ var _ = Describe("Operator Controller Test", func() { }) }) }) + When("a catalog changes on cluster", func() { + var testLogs, opNames []string + var cancel context.CancelFunc + var logCount int + BeforeEach(func() { + l := funcr.New(func(prefix, args string) { + if prefix == "operator-controller" && + strings.Contains(args, `"controller"="operator"`) && + strings.Contains(args, `"msg"="ending"`) { + // filter for only relevant logs + testLogs = append(testLogs, fmt.Sprintf("%s", args)) + } + }, funcr.Options{Verbosity: 1}) + mgr, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: sch, Logger: l}) + Expect(err).To(BeNil()) + + err = reconciler.SetupWithManager(mgr) + Expect(err).To(BeNil()) + var mgrCtx context.Context + mgrCtx, cancel = context.WithCancel(log.IntoContext(ctx, l)) + + go func() { + err := mgr.Start(mgrCtx) + Expect(err).To(BeNil()) + }() + + opNames = []string{"prometheus", "project-quay"} + for _, p := range opNames { + op := &operatorsv1alpha1.Operator{ObjectMeta: metav1.ObjectMeta{Name: p}, Spec: operatorsv1alpha1.OperatorSpec{PackageName: p}} + err := cl.Create(ctx, op) + Expect(err).To(BeNil()) + } + Eventually(func(g Gomega) { + By("verifying initial reconcile logs for operator creation") + g.Expect(len(testLogs) >= len(opNames)).To(BeTrue()) + for _, p := range opNames { + g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) + } + logCount = len(testLogs) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + }) + + It("reconciles all affected operators on cluster", func() { + By("creating a new catalog") + catalog := &catalogd.Catalog{ObjectMeta: metav1.ObjectMeta{Name: "t"}, Spec: catalogd.CatalogSpec{Source: catalogd.CatalogSource{Type: catalogd.SourceTypeImage, Image: &catalogd.ImageSource{}}}} + err := cl.Create(ctx, catalog) + Expect(err).To(BeNil()) + Eventually(func(g Gomega) { + By("verifying operator reconcile logs on catalog create") + g.Expect(len(testLogs)).To(Equal(logCount + len(opNames))) + for _, p := range opNames { + g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) + } + logCount = len(testLogs) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + + By("updating a catalog") + catalog.Spec.Source.Image.Ref = "s" + err = cl.Update(ctx, catalog) + Expect(err).To(BeNil()) + Eventually(func(g Gomega) { + By("verifying operator reconcile logs on catalog update") + g.Expect(len(testLogs)).To(Equal(logCount + len(opNames))) + for _, p := range opNames { + g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) + } + logCount = len(testLogs) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + + By("deleting a catalog") + err = cl.Delete(ctx, catalog) + Expect(err).To(BeNil()) + Eventually(func(g Gomega) { + By("verifying operator reconcile logs on catalog delete") + g.Expect(len(testLogs)).To(Equal(logCount + len(opNames))) + for _, p := range opNames { + g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) + } + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + }) + AfterEach(func() { + for _, p := range opNames { + op := &operatorsv1alpha1.Operator{ObjectMeta: metav1.ObjectMeta{Name: p}, Spec: operatorsv1alpha1.OperatorSpec{PackageName: p}} + Expect(cl.Delete(ctx, op)).To(BeNil()) + } + cancel() // stop manager + }) + }) }) func verifyInvariants(ctx context.Context, c client.Client, op *operatorsv1alpha1.Operator) { @@ -1095,6 +1188,12 @@ var testEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{ "olm.package": `{"packageName":"prometheus","version":"0.47.0"}`, "olm.gvk": `[]`, }), + "operatorhub/project-quay/3.8.3": *input.NewEntity("operatorhub/project-quay/3.8.3", map[string]string{ + "olm.bundle.path": `"quay.io/openshift-community-operators/project-quay@sha256:4f5698b5fec2e5f9a4df78b5ef9609b3a697c81cdb137b98e82e79104f0eb3b5"`, + "olm.channel": `{"channelName":"stable","priority":0}`, + "olm.package": `{"packageName":"project-quay","version":"3.8.3"}`, + "olm.gvk": `[]`, + }), "operatorhub/badimage/0.1.0": *input.NewEntity("operatorhub/badimage/0.1.0", map[string]string{ "olm.bundle.path": `{"name": "quay.io/operatorhubio/badimage:v0.1.0"}`, "olm.package": `{"packageName":"badimage","version":"0.1.0"}`, diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go index 7f56ce3e01..ec7ccea375 100644 --- a/internal/controllers/suite_test.go +++ b/internal/controllers/suite_test.go @@ -23,6 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" @@ -72,6 +73,8 @@ var _ = BeforeSuite(func() { Expect(err).NotTo(HaveOccurred()) err = rukpakv1alpha1.AddToScheme(sch) Expect(err).NotTo(HaveOccurred()) + err = catalogd.AddToScheme(sch) + Expect(err).NotTo(HaveOccurred()) cl, err = client.New(cfg, client.Options{Scheme: sch}) Expect(err).NotTo(HaveOccurred()) From 48b3c3f72462e96fdf905a289e82e7f332b153ed Mon Sep 17 00:00:00 2001 From: Ankita Thomas Date: Fri, 2 Jun 2023 11:15:25 -0700 Subject: [PATCH 2/3] remove e2e catalog reconcile test Signed-off-by: Ankita Thomas --- go.mod | 1 - .../controllers/operator_controller_test.go | 16 ++-- test/e2e/install_test.go | 79 +++++-------------- 3 files changed, 28 insertions(+), 68 deletions(-) diff --git a/go.mod b/go.mod index d2554dd1ee..ed4937988a 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/operator-framework/api v0.17.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect diff --git a/internal/controllers/operator_controller_test.go b/internal/controllers/operator_controller_test.go index 84a8d41c30..c6c8e38c54 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -9,7 +9,7 @@ import ( "github.com/go-logr/logr/funcr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/input" "github.com/operator-framework/deppy/pkg/deppy/solver" @@ -1101,7 +1101,7 @@ var _ = Describe("Operator Controller Test", func() { g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) } logCount = len(testLogs) - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) }) It("reconciles all affected operators on cluster", func() { @@ -1111,12 +1111,12 @@ var _ = Describe("Operator Controller Test", func() { Expect(err).To(BeNil()) Eventually(func(g Gomega) { By("verifying operator reconcile logs on catalog create") - g.Expect(len(testLogs)).To(Equal(logCount + len(opNames))) + g.Expect(testLogs).To(HaveLen(logCount + len(opNames))) for _, p := range opNames { g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) } logCount = len(testLogs) - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) By("updating a catalog") catalog.Spec.Source.Image.Ref = "s" @@ -1124,23 +1124,23 @@ var _ = Describe("Operator Controller Test", func() { Expect(err).To(BeNil()) Eventually(func(g Gomega) { By("verifying operator reconcile logs on catalog update") - g.Expect(len(testLogs)).To(Equal(logCount + len(opNames))) + g.Expect(testLogs).To(HaveLen(logCount + len(opNames))) for _, p := range opNames { g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) } logCount = len(testLogs) - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) By("deleting a catalog") err = cl.Delete(ctx, catalog) Expect(err).To(BeNil()) Eventually(func(g Gomega) { By("verifying operator reconcile logs on catalog delete") - g.Expect(len(testLogs)).To(Equal(logCount + len(opNames))) + g.Expect(testLogs).To(HaveLen(logCount + len(opNames))) for _, p := range opNames { g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) } - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).WithArguments().Should(Succeed()) + }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) }) AfterEach(func() { for _, p := range opNames { diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go index 7c78a33da3..a65b822be4 100644 --- a/test/e2e/install_test.go +++ b/test/e2e/install_test.go @@ -6,7 +6,6 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" operatorv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "k8s.io/apimachinery/pkg/api/errors" @@ -45,7 +44,6 @@ var _ = Describe("Operator Install", func() { By("eventually reporting a successful resolution and bundle path") Eventually(func(g Gomega) { g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed()) - 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)) @@ -66,9 +64,16 @@ var _ = Describe("Operator Install", func() { bd := rukpakv1alpha1.BundleDeployment{} g.Expect(c.Get(ctx, types.NamespacedName{Name: operatorName}, &bd)).To(Succeed()) - 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)) }).Should(Succeed()) }) }) @@ -86,7 +91,6 @@ var _ = Describe("Operator Install", func() { By("eventually reporting a successful resolution and bundle path") Eventually(func(g Gomega) { g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed()) - 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)) @@ -107,63 +111,20 @@ var _ = Describe("Operator Install", func() { bd := rukpakv1alpha1.BundleDeployment{} g.Expect(c.Get(ctx, types.NamespacedName{Name: operatorName}, &bd)).To(Succeed()) - 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")) - }).Should(Succeed()) - }) - }) - - It("resolves again when a new catalog is available", func() { - pkgName := "prometheus" - operator.Spec = operatorv1alpha1.OperatorSpec{ - PackageName: pkgName, - } - - // Delete the catalog first - Expect(c.Delete(ctx, operatorCatalog)).To(Succeed()) - - Eventually(func(g Gomega) { - // target package should not be present on cluster - err := c.Get(ctx, types.NamespacedName{Name: pkgName}, &catalogd.Package{}) - g.Expect(errors.IsNotFound(err)).To(BeTrue()) - }).Should(Succeed()) - - By("creating the Operator resource") - Expect(c.Create(ctx, operator)).To(Succeed()) - By("failing to find Operator during resolution") - Eventually(func(g Gomega) { - g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed()) - 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))) - }).Should(Succeed()) + 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)) - By("creating an Operator catalog with the desired package") - var err error - operatorCatalog, err = createTestCatalog(ctx, testCatalogName, testCatalogRef) - Expect(err).ToNot(HaveOccurred()) - Eventually(func(g Gomega) { - g.Expect(c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, operatorCatalog)).To(Succeed()) - 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)) - }).Should(Succeed()) + 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)) - By("eventually resolving the package successfully") - Eventually(func(g Gomega) { - g.Expect(c.Get(ctx, types.NamespacedName{Name: operator.Name}, operator)).To(Succeed()) - 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)) - }).Should(Succeed()) + }).Should(Succeed()) + }) }) - AfterEach(func() { Expect(c.Delete(ctx, operator)).To(Succeed()) Eventually(func(g Gomega) { From c2819f44cc658761d339f78fc4948a0c34bf0d9b Mon Sep 17 00:00:00 2001 From: Ankita Thomas Date: Mon, 5 Jun 2023 11:00:53 -0700 Subject: [PATCH 3/3] fake reconciler for catalog sync tests Signed-off-by: Ankita Thomas --- cmd/manager/main.go | 4 +- internal/controllers/catalog_test.go | 106 ++++++++++++++ internal/controllers/operator_controller.go | 2 +- .../controllers/operator_controller_test.go | 99 ------------- internal/controllers/suite_test.go | 2 +- ...atalogd.operatorframework.io_catalogs.yaml | 134 ++++++++++++++++++ 6 files changed, 244 insertions(+), 103 deletions(-) create mode 100644 internal/controllers/catalog_test.go create mode 100644 testdata/crds/catalogd.operatorframework.io_catalogs.yaml diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9cbe6ed4d2..f193ccacd3 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -94,14 +94,14 @@ func main() { os.Exit(1) } - if err = (&controllers.OperatorReconciler{ + if err = controllers.SetupWithManager(&controllers.OperatorReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Resolver: solver.NewDeppySolver( entitysources.NewCatalogdEntitySource(mgr.GetClient()), olm.NewOLMVariableSource(mgr.GetClient()), ), - }).SetupWithManager(mgr); err != nil { + }, mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Operator") os.Exit(1) } diff --git a/internal/controllers/catalog_test.go b/internal/controllers/catalog_test.go new file mode 100644 index 0000000000..8bcf7b4f16 --- /dev/null +++ b/internal/controllers/catalog_test.go @@ -0,0 +1,106 @@ +package controllers_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" + operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" + "github.com/operator-framework/operator-controller/internal/controllers" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func collectRequests(reqChan <-chan string) func() []string { + var reqNames []string + return func() []string { + select { + case req := <-reqChan: + reqNames = append(reqNames, req) + default: + } + return reqNames + } +} + +func operatorForPackage(name, pkg string) *operatorsv1alpha1.Operator { + return &operatorsv1alpha1.Operator{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: operatorsv1alpha1.OperatorSpec{ + PackageName: pkg, + }, + } +} + +var _ = Describe("SetupWithManager", func() { + When("catalog events occur", func() { + var cancel context.CancelFunc + var ctx context.Context + var opNames []string + var reqChan chan string + BeforeEach(func() { + ctx = context.Background() + mgr, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: sch}) + Expect(err).To(BeNil()) + + opNames = []string{"prometheus", "project-quay"} + reqChan = make(chan string) + var fakeReconciler reconcile.Func = func(_ context.Context, request ctrl.Request) (ctrl.Result, error) { + reqChan <- request.Name + return ctrl.Result{}, nil + } + Expect(controllers.SetupWithManager(fakeReconciler, mgr)).To(Succeed()) + + var mgrCtx context.Context + mgrCtx, cancel = context.WithCancel(ctx) + go func() { + Expect(mgr.Start(mgrCtx)).To(Succeed()) + }() + + for _, p := range opNames { + Expect(cl.Create(ctx, operatorForPackage(p, p))).To(Succeed()) + } + By("verifying initial reconcile logs for operator creation") + Eventually(collectRequests(reqChan)).Should(ConsistOf(opNames)) + }) + It("reconciles all affected operators on cluster", func() { + By("creating a new catalog") + catalog := &catalogd.Catalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: "t", + }, + Spec: catalogd.CatalogSpec{ + Source: catalogd.CatalogSource{ + Type: catalogd.SourceTypeImage, + Image: &catalogd.ImageSource{}, + }, + }, + } + Expect(cl.Create(ctx, catalog)).To(Succeed()) + By("verifying operator reconcile logs on catalog create") + Eventually(collectRequests(reqChan)).Should(ConsistOf(opNames)) + + By("updating a catalog") + catalog.Spec.Source.Image.Ref = "s" + Expect(cl.Update(ctx, catalog)).To(Succeed()) + By("verifying operator reconcile logs on catalog update") + Eventually(collectRequests(reqChan)).Should(ConsistOf(opNames)) + + By("deleting a catalog") + Expect(cl.Delete(ctx, catalog)).To(Succeed()) + By("verifying operator reconcile logs on catalog delete") + Eventually(collectRequests(reqChan)).Should(ConsistOf(opNames)) + }) + AfterEach(func() { + cancel() // stop manager + close(reqChan) + for _, p := range opNames { + Expect(cl.Delete(ctx, operatorForPackage(p, p))).To(Succeed()) + } + }) + }) +}) diff --git a/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index b0c599f169..312a51fced 100644 --- a/internal/controllers/operator_controller.go +++ b/internal/controllers/operator_controller.go @@ -298,7 +298,7 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha } // SetupWithManager sets up the controller with the Manager. -func (r *OperatorReconciler) SetupWithManager(mgr ctrl.Manager) error { +func SetupWithManager(r reconcile.Reconciler, mgr ctrl.Manager) error { err := ctrl.NewControllerManagedBy(mgr). For(&operatorsv1alpha1.Operator{}). Watches(source.NewKindWithCache(&catalogd.Catalog{}, mgr.GetCache()), diff --git a/internal/controllers/operator_controller_test.go b/internal/controllers/operator_controller_test.go index c6c8e38c54..8e75327b73 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -3,13 +3,9 @@ package controllers_test import ( "context" "fmt" - "strings" - "time" - "github.com/go-logr/logr/funcr" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/input" "github.com/operator-framework/deppy/pkg/deppy/solver" @@ -22,7 +18,6 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" - "sigs.k8s.io/controller-runtime/pkg/log" operatorsv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1" "github.com/operator-framework/operator-controller/internal/conditionsets" @@ -1062,94 +1057,6 @@ var _ = Describe("Operator Controller Test", func() { }) }) }) - When("a catalog changes on cluster", func() { - var testLogs, opNames []string - var cancel context.CancelFunc - var logCount int - BeforeEach(func() { - l := funcr.New(func(prefix, args string) { - if prefix == "operator-controller" && - strings.Contains(args, `"controller"="operator"`) && - strings.Contains(args, `"msg"="ending"`) { - // filter for only relevant logs - testLogs = append(testLogs, fmt.Sprintf("%s", args)) - } - }, funcr.Options{Verbosity: 1}) - mgr, err := ctrl.NewManager(cfg, ctrl.Options{Scheme: sch, Logger: l}) - Expect(err).To(BeNil()) - - err = reconciler.SetupWithManager(mgr) - Expect(err).To(BeNil()) - var mgrCtx context.Context - mgrCtx, cancel = context.WithCancel(log.IntoContext(ctx, l)) - - go func() { - err := mgr.Start(mgrCtx) - Expect(err).To(BeNil()) - }() - - opNames = []string{"prometheus", "project-quay"} - for _, p := range opNames { - op := &operatorsv1alpha1.Operator{ObjectMeta: metav1.ObjectMeta{Name: p}, Spec: operatorsv1alpha1.OperatorSpec{PackageName: p}} - err := cl.Create(ctx, op) - Expect(err).To(BeNil()) - } - Eventually(func(g Gomega) { - By("verifying initial reconcile logs for operator creation") - g.Expect(len(testLogs) >= len(opNames)).To(BeTrue()) - for _, p := range opNames { - g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) - } - logCount = len(testLogs) - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) - }) - - It("reconciles all affected operators on cluster", func() { - By("creating a new catalog") - catalog := &catalogd.Catalog{ObjectMeta: metav1.ObjectMeta{Name: "t"}, Spec: catalogd.CatalogSpec{Source: catalogd.CatalogSource{Type: catalogd.SourceTypeImage, Image: &catalogd.ImageSource{}}}} - err := cl.Create(ctx, catalog) - Expect(err).To(BeNil()) - Eventually(func(g Gomega) { - By("verifying operator reconcile logs on catalog create") - g.Expect(testLogs).To(HaveLen(logCount + len(opNames))) - for _, p := range opNames { - g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) - } - logCount = len(testLogs) - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) - - By("updating a catalog") - catalog.Spec.Source.Image.Ref = "s" - err = cl.Update(ctx, catalog) - Expect(err).To(BeNil()) - Eventually(func(g Gomega) { - By("verifying operator reconcile logs on catalog update") - g.Expect(testLogs).To(HaveLen(logCount + len(opNames))) - for _, p := range opNames { - g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) - } - logCount = len(testLogs) - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) - - By("deleting a catalog") - err = cl.Delete(ctx, catalog) - Expect(err).To(BeNil()) - Eventually(func(g Gomega) { - By("verifying operator reconcile logs on catalog delete") - g.Expect(testLogs).To(HaveLen(logCount + len(opNames))) - for _, p := range opNames { - g.Expect(testLogs[len(testLogs)-len(opNames):]).To(ContainElement(ContainSubstring(fmt.Sprintf("\"Operator\"={\"name\":\"%s\"}", p)))) - } - }).WithTimeout(2 * time.Second).WithPolling(1 * time.Second).Should(Succeed()) - }) - AfterEach(func() { - for _, p := range opNames { - op := &operatorsv1alpha1.Operator{ObjectMeta: metav1.ObjectMeta{Name: p}, Spec: operatorsv1alpha1.OperatorSpec{PackageName: p}} - Expect(cl.Delete(ctx, op)).To(BeNil()) - } - cancel() // stop manager - }) - }) }) func verifyInvariants(ctx context.Context, c client.Client, op *operatorsv1alpha1.Operator) { @@ -1188,12 +1095,6 @@ var testEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{ "olm.package": `{"packageName":"prometheus","version":"0.47.0"}`, "olm.gvk": `[]`, }), - "operatorhub/project-quay/3.8.3": *input.NewEntity("operatorhub/project-quay/3.8.3", map[string]string{ - "olm.bundle.path": `"quay.io/openshift-community-operators/project-quay@sha256:4f5698b5fec2e5f9a4df78b5ef9609b3a697c81cdb137b98e82e79104f0eb3b5"`, - "olm.channel": `{"channelName":"stable","priority":0}`, - "olm.package": `{"packageName":"project-quay","version":"3.8.3"}`, - "olm.gvk": `[]`, - }), "operatorhub/badimage/0.1.0": *input.NewEntity("operatorhub/badimage/0.1.0", map[string]string{ "olm.bundle.path": `{"name": "quay.io/operatorhubio/badimage:v0.1.0"}`, "olm.package": `{"packageName":"badimage","version":"0.1.0"}`, diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go index ec7ccea375..da391dc6d7 100644 --- a/internal/controllers/suite_test.go +++ b/internal/controllers/suite_test.go @@ -23,7 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - catalogd "github.com/operator-framework/catalogd/pkg/apis/core/v1beta1" + catalogd "github.com/operator-framework/catalogd/api/core/v1alpha1" rukpakv1alpha1 "github.com/operator-framework/rukpak/api/v1alpha1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" diff --git a/testdata/crds/catalogd.operatorframework.io_catalogs.yaml b/testdata/crds/catalogd.operatorframework.io_catalogs.yaml new file mode 100644 index 0000000000..fb7be0b45f --- /dev/null +++ b/testdata/crds/catalogd.operatorframework.io_catalogs.yaml @@ -0,0 +1,134 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.11.4 + name: catalogs.catalogd.operatorframework.io +spec: + group: catalogd.operatorframework.io + names: + kind: Catalog + listKind: CatalogList + plural: catalogs + singular: catalog + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: Catalog is the Schema for the Catalogs API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: CatalogSpec defines the desired state of Catalog + properties: + source: + description: Source is the source of a Catalog that contains Operators' metadata in the FBC format https://olm.operatorframework.io/docs/reference/file-based-catalogs/#docs + oneOf: + - required: + - image + properties: + image: + description: Image is the catalog image that backs the content of this catalog. + properties: + pullSecret: + description: PullSecret contains the name of the image pull secret in the namespace that catalogd is deployed. + type: string + ref: + description: Ref contains the reference to a container image containing Catalog contents. + type: string + required: + - ref + type: object + type: + description: Type defines the kind of Catalog content being sourced. + type: string + required: + - type + type: object + required: + - source + type: object + status: + description: CatalogStatus defines the observed state of Catalog + properties: + conditions: + description: Conditions store the status conditions of the Catalog instances + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + phase: + type: string + resolvedSource: + description: CatalogSource contains the sourcing information for a Catalog + properties: + image: + description: Image is the catalog image that backs the content of this catalog. + properties: + pullSecret: + description: PullSecret contains the name of the image pull secret in the namespace that catalogd is deployed. + type: string + ref: + description: Ref contains the reference to a container image containing Catalog contents. + type: string + required: + - ref + type: object + type: + description: Type defines the kind of Catalog content being sourced. + type: string + required: + - type + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {}