diff --git a/Makefile b/Makefile index 5d8d4d8d8..d6f19f2ab 100644 --- a/Makefile +++ b/Makefile @@ -105,8 +105,10 @@ kind-cluster-cleanup: $(KIND) ## Delete the kind cluster kind-load-test-artifacts: $(KIND) ## Load the e2e testdata container images into a kind cluster $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/registry-v1/prometheus-operator.v0.47.0 -t localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 + $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/bundles/plain-v0/plain.v0.1.0 -t localhost/testdata/bundles/plain-v0/plain:v0.1.0 $(CONTAINER_RUNTIME) build $(TESTDATA_DIR)/catalogs -f $(TESTDATA_DIR)/catalogs/test-catalog.Dockerfile -t localhost/testdata/catalogs/test-catalog:e2e $(KIND) load docker-image localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 --name $(KIND_CLUSTER_NAME) + $(KIND) load docker-image localhost/testdata/bundles/plain-v0/plain:v0.1.0 --name $(KIND_CLUSTER_NAME) $(KIND) load docker-image localhost/testdata/catalogs/test-catalog:e2e --name $(KIND_CLUSTER_NAME) ##@ Build diff --git a/internal/controllers/operator_controller.go b/internal/controllers/operator_controller.go index 5e8c03b9c..b0c599f16 100644 --- a/internal/controllers/operator_controller.go +++ b/internal/controllers/operator_controller.go @@ -156,9 +156,19 @@ func (r *OperatorReconciler) reconcile(ctx context.Context, op *operatorsv1alpha op.Status.ResolvedBundleResource = bundleImage setResolvedStatusConditionSuccess(&op.Status.Conditions, fmt.Sprintf("resolved to %q", bundleImage), op.GetGeneration()) + mediaType, err := bundleEntity.MediaType() + if err != nil { + setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration()) + return ctrl.Result{}, err + } + bundleProvisioner, err := mapBundleMediaTypeToBundleProvisioner(mediaType) + if err != nil { + setInstalledStatusConditionFailed(&op.Status.Conditions, err.Error(), op.GetGeneration()) + return ctrl.Result{}, err + } // Ensure a BundleDeployment exists with its bundle source from the bundle // image we just looked up in the solution. - dep := r.generateExpectedBundleDeployment(*op, bundleImage) + dep := r.generateExpectedBundleDeployment(*op, bundleImage, bundleProvisioner) if err := r.ensureBundleDeployment(ctx, dep); err != nil { // originally Reason: operatorsv1alpha1.ReasonInstallationFailed op.Status.InstalledBundleResource = "" @@ -244,12 +254,13 @@ func (r *OperatorReconciler) getBundleEntityFromSolution(solution *solver.Soluti return nil, fmt.Errorf("entity for package %q not found in solution", packageName) } -func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundlePath string) *unstructured.Unstructured { +func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha1.Operator, bundlePath string, bundleProvisioner string) *unstructured.Unstructured { // We use unstructured here to avoid problems of serializing default values when sending patches to the apiserver. // If you use a typed object, any default values from that struct get serialized into the JSON patch, which could // cause unrelated fields to be patched back to the default value even though that isn't the intention. Using an // unstructured ensures that the patch contains only what is specified. Using unstructured like this is basically // identical to "kubectl apply -f" + bd := &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": rukpakv1alpha1.GroupVersion.String(), "kind": rukpakv1alpha1.BundleDeploymentKind, @@ -261,8 +272,7 @@ func (r *OperatorReconciler) generateExpectedBundleDeployment(o operatorsv1alpha "provisionerClassName": "core-rukpak-io-plain", "template": map[string]interface{}{ "spec": map[string]interface{}{ - // TODO: Don't assume registry provisioner - "provisionerClassName": "core-rukpak-io-registry", + "provisionerClassName": bundleProvisioner, "source": map[string]interface{}{ // TODO: Don't assume image type "type": string(rukpakv1alpha1.SourceTypeImage), @@ -363,6 +373,23 @@ func isBundleDepStale(bd *rukpakv1alpha1.BundleDeployment) bool { return bd != nil && bd.Status.ObservedGeneration != bd.GetGeneration() } +// mapBundleMediaTypeToBundleProvisioner maps an olm.bundle.mediatype property to a +// rukpak bundle provisioner class name that is capable of unpacking the bundle type +func mapBundleMediaTypeToBundleProvisioner(mediaType string) (string, error) { + switch mediaType { + case entity.MediaTypePlain: + return "core-rukpak-io-plain", nil + // To ensure compatibility with bundles created with OLMv0 where the + // olm.bundle.mediatype property doesn't exist, we assume that if the + // property is empty (i.e doesn't exist) that the bundle is one created + // with OLMv0 and therefore should use the registry provisioner + case entity.MediaTypeRegistry, "": + return "core-rukpak-io-registry", nil + default: + return "", fmt.Errorf("unknown bundle mediatype: %s", mediaType) + } +} + // setResolvedStatusConditionSuccess sets the resolved status condition to success. func setResolvedStatusConditionSuccess(conditions *[]metav1.Condition, message string, generation int64) { apimeta.SetStatusCondition(conditions, metav1.Condition{ diff --git a/internal/controllers/operator_controller_test.go b/internal/controllers/operator_controller_test.go index e861cc033..8e75327b7 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -904,6 +904,107 @@ var _ = Describe("Operator Controller Test", func() { Expect(cond.Message).To(Equal("installation has not been attempted as resolution failed")) }) }) + When("the operator specifies a package with a plain+v0 bundle", func() { + var pkgName string + var pkgVer string + var pkgChan string + BeforeEach(func() { + By("initializing cluster state") + pkgName = "plain" + pkgVer = "0.1.0" + pkgChan = "beta" + operator = &operatorsv1alpha1.Operator{ + ObjectMeta: metav1.ObjectMeta{Name: opKey.Name}, + Spec: operatorsv1alpha1.OperatorSpec{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + } + err := cl.Create(ctx, operator) + Expect(err).NotTo(HaveOccurred()) + }) + It("sets resolution success status", func() { + By("running reconcile") + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey}) + Expect(res).To(Equal(ctrl.Result{})) + Expect(err).NotTo(HaveOccurred()) + By("fetching updated operator after reconcile") + Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred()) + + By("Checking the status fields") + Expect(operator.Status.ResolvedBundleResource).To(Equal("quay.io/operatorhub/plain@sha256:plain")) + Expect(operator.Status.InstalledBundleResource).To(Equal("")) + + By("checking the expected conditions") + cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved) + Expect(cond).NotTo(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonSuccess)) + Expect(cond.Message).To(Equal("resolved to \"quay.io/operatorhub/plain@sha256:plain\"")) + cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeInstalled) + Expect(cond).NotTo(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionUnknown)) + Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonInstallationStatusUnknown)) + Expect(cond.Message).To(Equal("bundledeployment status is unknown")) + + By("fetching the bundled deployment") + bd := &rukpakv1alpha1.BundleDeployment{} + Expect(cl.Get(ctx, types.NamespacedName{Name: opKey.Name}, bd)).NotTo(HaveOccurred()) + Expect(bd.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain")) + Expect(bd.Spec.Template.Spec.ProvisionerClassName).To(Equal("core-rukpak-io-plain")) + Expect(bd.Spec.Template.Spec.Source.Type).To(Equal(rukpakv1alpha1.SourceTypeImage)) + Expect(bd.Spec.Template.Spec.Source.Image).NotTo(BeNil()) + Expect(bd.Spec.Template.Spec.Source.Image.Ref).To(Equal("quay.io/operatorhub/plain@sha256:plain")) + }) + }) + When("the operator specifies a package with a bad bundle mediatype", func() { + var pkgName string + var pkgVer string + var pkgChan string + BeforeEach(func() { + By("initializing cluster state") + pkgName = "badmedia" + pkgVer = "0.1.0" + pkgChan = "beta" + operator = &operatorsv1alpha1.Operator{ + ObjectMeta: metav1.ObjectMeta{Name: opKey.Name}, + Spec: operatorsv1alpha1.OperatorSpec{ + PackageName: pkgName, + Version: pkgVer, + Channel: pkgChan, + }, + } + err := cl.Create(ctx, operator) + Expect(err).NotTo(HaveOccurred()) + }) + It("sets resolution success status", func() { + By("running reconcile") + res, err := reconciler.Reconcile(ctx, ctrl.Request{NamespacedName: opKey}) + Expect(res).To(Equal(ctrl.Result{})) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal("unknown bundle mediatype: badmedia+v1")) + + By("fetching updated operator after reconcile") + Expect(cl.Get(ctx, opKey, operator)).NotTo(HaveOccurred()) + + By("Checking the status fields") + Expect(operator.Status.ResolvedBundleResource).To(Equal("quay.io/operatorhub/badmedia@sha256:badmedia")) + Expect(operator.Status.InstalledBundleResource).To(Equal("")) + + By("checking the expected conditions") + cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeResolved) + Expect(cond).NotTo(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonSuccess)) + Expect(cond.Message).To(Equal("resolved to \"quay.io/operatorhub/badmedia@sha256:badmedia\"")) + cond = apimeta.FindStatusCondition(operator.Status.Conditions, operatorsv1alpha1.TypeInstalled) + Expect(cond).NotTo(BeNil()) + Expect(cond.Status).To(Equal(metav1.ConditionFalse)) + Expect(cond.Reason).To(Equal(operatorsv1alpha1.ReasonInstallationFailed)) + Expect(cond.Message).To(Equal("unknown bundle mediatype: badmedia+v1")) + }) + }) When("an invalid semver is provided that bypasses the regex validation", func() { var ( pkgName string @@ -955,7 +1056,6 @@ var _ = Describe("Operator Controller Test", func() { Expect(cond.Message).To(Equal("installation has not been attempted as spec is invalid")) }) }) - }) }) @@ -1000,4 +1100,18 @@ var testEntitySource = input.NewCacheQuerier(map[deppy.Identifier]input.Entity{ "olm.package": `{"packageName":"badimage","version":"0.1.0"}`, "olm.gvk": `[]`, }), + "operatorhub/plain/0.1.0": *input.NewEntity("operatorhub/plain/0.1.0", map[string]string{ + "olm.bundle.path": `"quay.io/operatorhub/plain@sha256:plain"`, + "olm.channel": `{"channelName":"beta","priority":0}`, + "olm.package": `{"packageName":"plain","version":"0.1.0"}`, + "olm.gvk": `[]`, + "olm.bundle.mediatype": `"plain+v0"`, + }), + "operatorhub/badmedia/0.1.0": *input.NewEntity("operatorhub/badmedia/0.1.0", map[string]string{ + "olm.bundle.path": `"quay.io/operatorhub/badmedia@sha256:badmedia"`, + "olm.channel": `{"channelName":"beta","priority":0}`, + "olm.package": `{"packageName":"badmedia","version":"0.1.0"}`, + "olm.gvk": `[]`, + "olm.bundle.mediatype": `"badmedia+v1"`, + }), }) diff --git a/internal/resolution/entitysources/catalogdsource.go b/internal/resolution/entitysources/catalogdsource.go index 9cc74ed55..b1ebc77c5 100644 --- a/internal/resolution/entitysources/catalogdsource.go +++ b/internal/resolution/entitysources/catalogdsource.go @@ -7,6 +7,7 @@ import ( "github.com/operator-framework/deppy/pkg/deppy" "github.com/operator-framework/deppy/pkg/deppy/input" + "github.com/operator-framework/operator-controller/internal/resolution/variable_sources/entity" "github.com/operator-framework/operator-registry/alpha/property" "sigs.k8s.io/controller-runtime/pkg/client" @@ -80,12 +81,16 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e for _, bundle := range bundleMetadatas.Items { props := map[string]string{} + // TODO: We should make sure all properties are forwarded + // through and avoid a lossy translation from FBC --> entity for _, prop := range bundle.Spec.Properties { switch prop.Type { case property.TypePackage: // this is already a json marshalled object, so it doesn't need to be marshalled // like the other ones props[property.TypePackage] = string(prop.Value) + case entity.PropertyBundleMediaType: + props[entity.PropertyBundleMediaType] = string(prop.Value) } } diff --git a/internal/resolution/variable_sources/entity/bundle_entity.go b/internal/resolution/variable_sources/entity/bundle_entity.go index cadb79d65..7c3958ede 100644 --- a/internal/resolution/variable_sources/entity/bundle_entity.go +++ b/internal/resolution/variable_sources/entity/bundle_entity.go @@ -12,6 +12,19 @@ import ( const PropertyBundlePath = "olm.bundle.path" +// TODO: Is this the right place for these? +// ---- +const PropertyBundleMediaType = "olm.bundle.mediatype" + +type MediaType string + +const ( + MediaTypePlain = "plain+v0" + MediaTypeRegistry = "registry+v1" +) + +// ---- + type ChannelProperties struct { property.Channel Replaces string `json:"replaces,omitempty"` @@ -58,6 +71,7 @@ type BundleEntity struct { channelProperties *ChannelProperties semVersion *semver.Version bundlePath string + mediaType string mu sync.RWMutex } @@ -124,6 +138,27 @@ func (b *BundleEntity) BundlePath() (string, error) { return b.bundlePath, nil } +func (b *BundleEntity) MediaType() (string, error) { + if err := b.loadMediaType(); err != nil { + return "", err + } + + return b.mediaType, nil +} + +func (b *BundleEntity) loadMediaType() error { + b.mu.Lock() + defer b.mu.Unlock() + if b.mediaType == "" { + mediaType, err := loadFromEntity[string](b.Entity, PropertyBundleMediaType, optional) + if err != nil { + return fmt.Errorf("error determining bundle mediatype for entity '%s': %w", b.ID, err) + } + b.mediaType = mediaType + } + return nil +} + func (b *BundleEntity) loadPackage() error { b.mu.Lock() defer b.mu.Unlock() diff --git a/internal/resolution/variable_sources/entity/bundle_entity_test.go b/internal/resolution/variable_sources/entity/bundle_entity_test.go index 16aa5e87c..d904610e2 100644 --- a/internal/resolution/variable_sources/entity/bundle_entity_test.go +++ b/internal/resolution/variable_sources/entity/bundle_entity_test.go @@ -1,6 +1,7 @@ package entity_test import ( + "fmt" "testing" "github.com/blang/semver/v4" @@ -267,4 +268,32 @@ var _ = Describe("BundleEntity", func() { Expect(err.Error()).To(Equal("error determining bundle path for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.path' ('badBundlePath') could not be parsed: invalid character 'b' looking for beginning of value")) }) }) + + Describe("MediaType", func() { + It("should return the bundle mediatype property if present", func() { + entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ + olmentity.PropertyBundleMediaType: fmt.Sprintf(`"%s"`, olmentity.MediaTypePlain), + }) + bundleEntity := olmentity.NewBundleEntity(entity) + mediaType, err := bundleEntity.MediaType() + Expect(err).ToNot(HaveOccurred()) + Expect(mediaType).To(Equal(olmentity.MediaTypePlain)) + }) + It("should not return an error if the property is not found", func() { + entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{}) + bundleEntity := olmentity.NewBundleEntity(entity) + mediaType, err := bundleEntity.MediaType() + Expect(mediaType).To(BeEmpty()) + Expect(err).To(BeNil()) + }) + It("should return error if the property is malformed", func() { + entity := input.NewEntity("operatorhub/prometheus/0.14.0", map[string]string{ + olmentity.PropertyBundleMediaType: "badtype", + }) + bundleEntity := olmentity.NewBundleEntity(entity) + mediaType, err := bundleEntity.MediaType() + Expect(mediaType).To(BeEmpty()) + Expect(err.Error()).To(Equal("error determining bundle mediatype for entity 'operatorhub/prometheus/0.14.0': property 'olm.bundle.mediatype' ('badtype') could not be parsed: invalid character 'b' looking for beginning of value")) + }) + }) }) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index d1e716276..4df4ad81a 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -1,12 +1,17 @@ package e2e import ( + "context" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "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/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -17,8 +22,14 @@ import ( ) var ( - cfg *rest.Config - c client.Client + cfg *rest.Config + c client.Client + operatorCatalog *catalogd.Catalog +) + +const ( + testCatalogRef = "localhost/testdata/catalogs/test-catalog:e2e" + testCatalogName = "test-catalog" ) func TestE2E(t *testing.T) { @@ -42,4 +53,71 @@ var _ = BeforeSuite(func() { Expect(err).ToNot(HaveOccurred()) c, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).To(Not(HaveOccurred())) + + ctx := context.Background() + operatorCatalog, err = createTestCatalog(ctx, testCatalogName, testCatalogRef) + 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)) + + // Ensure some packages exist before continuing so the + // operators don't get stuck in a bad state + pList := &catalogd.PackageList{} + err = c.List(ctx, pList) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(pList.Items).To(HaveLen(2)) + }).Should(Succeed()) }) + +var _ = AfterSuite(func() { + ctx := context.Background() + + Expect(c.Delete(ctx, operatorCatalog)).To(Succeed()) + Eventually(func(g Gomega) { + err := c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, &catalogd.Catalog{}) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) + + // speed up delete without waiting for gc + Expect(c.DeleteAllOf(ctx, &catalogd.BundleMetadata{})).To(Succeed()) + Expect(c.DeleteAllOf(ctx, &catalogd.Package{})).To(Succeed()) + + Eventually(func(g Gomega) { + // ensure resource cleanup + packages := &catalogd.PackageList{} + g.Expect(c.List(ctx, packages)).To(Succeed()) + g.Expect(packages.Items).To(BeEmpty()) + + bmd := &catalogd.BundleMetadataList{} + g.Expect(c.List(ctx, bmd)).To(Succeed()) + g.Expect(bmd.Items).To(BeEmpty()) + }).Should(Succeed()) +}) + +// createTestCatalog will create a new catalog on the test cluster, provided +// the context, catalog name, and the image reference. It returns the created catalog +// or an error if any errors occurred while creating the catalog. +func createTestCatalog(ctx context.Context, name string, imageRef string) (*catalogd.Catalog, error) { + catalog := &catalogd.Catalog{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + Spec: catalogd.CatalogSpec{ + Source: catalogd.CatalogSource{ + Type: catalogd.SourceTypeImage, + Image: &catalogd.ImageSource{ + Ref: imageRef, + }, + }, + }, + } + + err := c.Create(ctx, catalog) + return catalog, err +} diff --git a/test/e2e/install_test.go b/test/e2e/install_test.go index 1663c4c35..7c78a33da 100644 --- a/test/e2e/install_test.go +++ b/test/e2e/install_test.go @@ -3,7 +3,6 @@ package e2e import ( "context" "fmt" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -17,184 +16,161 @@ import ( "k8s.io/apimachinery/pkg/util/rand" ) -const ( - defaultTimeout = 30 * time.Second - defaultPoll = 1 * time.Second - testCatalogRef = "localhost/testdata/catalogs/test-catalog:e2e" - testCatalogName = "test-catalog" -) - var _ = Describe("Operator Install", func() { var ( - ctx context.Context - pkgName string - operatorName string - operator *operatorv1alpha1.Operator - operatorCatalog *catalogd.Catalog + ctx context.Context + operatorName string + operator *operatorv1alpha1.Operator ) When("An operator is installed from an operator catalog", func() { BeforeEach(func() { ctx = context.Background() - pkgName = "prometheus" operatorName = fmt.Sprintf("operator-%s", rand.String(8)) operator = &operatorv1alpha1.Operator{ ObjectMeta: metav1.ObjectMeta{ Name: operatorName, }, - Spec: operatorv1alpha1.OperatorSpec{ - PackageName: pkgName, - }, - } - operatorCatalog = &catalogd.Catalog{ - ObjectMeta: metav1.ObjectMeta{ - Name: testCatalogName, - }, - Spec: catalogd.CatalogSpec{ - Source: catalogd.CatalogSource{ - Type: catalogd.SourceTypeImage, - Image: &catalogd.ImageSource{ - Ref: testCatalogRef, - }, - }, - }, } }) - 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: testCatalogName}, operatorCatalog) - g.Expect(err).ToNot(HaveOccurred()) - g.Expect(len(operatorCatalog.Status.Conditions)).To(Equal(1)) - 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(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) - - By("creating the Operator resource") - 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()) + When("the operator bundle format is registry+v1", func() { + BeforeEach(func() { + operator.Spec = operatorv1alpha1.OperatorSpec{ + PackageName: "prometheus", + } + }) + It("resolves the specified package with correct bundle path", func() { + By("creating the Operator resource") + Expect(c.Create(ctx, operator)).To(Succeed()) + + 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)) + g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess)) + g.Expect(cond.Message).To(ContainSubstring("resolved to")) + g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty()) + }).Should(Succeed()) + + By("eventually installing 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.TypeInstalled) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess)) + g.Expect(cond.Message).To(ContainSubstring("installed from")) + g.Expect(operator.Status.InstalledBundleResource).ToNot(BeEmpty()) + + 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()) + }) + }) - 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)) - g.Expect(cond.Message).To(ContainSubstring("resolved to")) - g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty()) - }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + When("the operator bundle format is plain+v0", func() { + BeforeEach(func() { + operator.Spec = operatorv1alpha1.OperatorSpec{ + PackageName: "plain", + } + }) + It("resolves the specified package with correct bundle path", func() { + By("creating the Operator resource") + Expect(c.Create(ctx, operator)).To(Succeed()) + + 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)) + g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess)) + g.Expect(cond.Message).To(ContainSubstring("resolved to")) + g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty()) + }).Should(Succeed()) + + By("eventually installing 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.TypeInstalled) + g.Expect(cond).ToNot(BeNil()) + g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) + g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess)) + g.Expect(cond.Message).To(ContainSubstring("installed from")) + g.Expect(operator.Status.InstalledBundleResource).ToNot(BeEmpty()) + + 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()) + }) + }) - By("eventually installing 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.TypeInstalled) - g.Expect(cond).ToNot(BeNil()) - g.Expect(cond.Status).To(Equal(metav1.ConditionTrue)) - g.Expect(cond.Reason).To(Equal(operatorv1alpha1.ReasonSuccess)) - g.Expect(cond.Message).To(ContainSubstring("installed from")) - g.Expect(operator.Status.InstalledBundleResource).ToNot(BeEmpty()) - bd := rukpakv1alpha1.BundleDeployment{} - err = c.Get(ctx, types.NamespacedName{Name: operatorName}, &bd) - g.Expect(err).ToNot(HaveOccurred()) + It("resolves again when a new catalog is available", func() { + pkgName := "prometheus" + operator.Spec = operatorv1alpha1.OperatorSpec{ + PackageName: pkgName, + } - 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)) + // Delete the catalog first + Expect(c.Delete(ctx, operatorCatalog)).To(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)) - }).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()) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) By("creating the Operator resource") - err := c.Create(ctx, operator) - Expect(err).ToNot(HaveOccurred()) + Expect(c.Create(ctx, operator)).To(Succeed()) 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()) + 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))) - }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + }).Should(Succeed()) By("creating an Operator catalog with the desired package") - err = c.Create(ctx, operatorCatalog) + var err error + operatorCatalog, err = createTestCatalog(ctx, testCatalogName, testCatalogRef) Expect(err).ToNot(HaveOccurred()) Eventually(func(g Gomega) { - err = c.Get(ctx, types.NamespacedName{Name: operatorCatalog.Name}, operatorCatalog) - g.Expect(err).ToNot(HaveOccurred()) + 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)) - }).WithTimeout(5 * time.Minute).WithPolling(defaultPoll).Should(Succeed()) + }).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()) + 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)) - }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + }).Should(Succeed()) }) - AfterEach(func() { - 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.DeleteAllOf(ctx, &catalogd.Package{}) - Expect(err).ToNot(HaveOccurred()) + AfterEach(func() { + Expect(c.Delete(ctx, operator)).To(Succeed()) 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()) + err := c.Get(ctx, types.NamespacedName{Name: operator.Name}, &operatorv1alpha1.Operator{}) + g.Expect(errors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) }) + }) }) diff --git a/testdata/bundles/plain-v0/plain.v0.1.0/Dockerfile b/testdata/bundles/plain-v0/plain.v0.1.0/Dockerfile new file mode 100644 index 000000000..bd54f66e6 --- /dev/null +++ b/testdata/bundles/plain-v0/plain.v0.1.0/Dockerfile @@ -0,0 +1,2 @@ +FROM scratch +COPY manifests /manifests \ No newline at end of file diff --git a/testdata/bundles/plain-v0/plain.v0.1.0/manifests/configmap.yaml b/testdata/bundles/plain-v0/plain.v0.1.0/manifests/configmap.yaml new file mode 100644 index 000000000..268033efa --- /dev/null +++ b/testdata/bundles/plain-v0/plain.v0.1.0/manifests/configmap.yaml @@ -0,0 +1,19 @@ +# This configmap was pulled straight from the Kubernetes docs +# and can be found at https://kubernetes.io/docs/concepts/configuration/configmap/ +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-demo +data: + # property-like keys; each key maps to a simple value + player_initial_lives: "3" + ui_properties_file_name: "user-interface.properties" + + # file-like keys + game.properties: | + enemy.types=aliens,monsters + player.maximum-lives=5 + user-interface.properties: | + color.good=purple + color.bad=yellow + allow.textmode=true \ No newline at end of file diff --git a/testdata/catalogs/test-catalog/catalog.json b/testdata/catalogs/test-catalog/catalog.json deleted file mode 100644 index 7e4e9a6a3..000000000 --- a/testdata/catalogs/test-catalog/catalog.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "schema": "olm.package", - "name": "prometheus", - "defaultChannel": "beta" -} -{ - "schema": "olm.channel", - "name": "beta", - "package": "prometheus", - "entries": [ - { - "name": "prometheusoperator.0.47.0" - } - ] -} -{ - "schema": "olm.bundle", - "name": "prometheusoperator.0.47.0", - "package": "prometheus", - "image": "localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0", - "properties": [ - { - "type": "olm.package", - "value": { - "packageName": "prometheus", - "version": "0.47.0" - } - } - ] -} diff --git a/testdata/catalogs/test-catalog/catalog.yaml b/testdata/catalogs/test-catalog/catalog.yaml new file mode 100644 index 000000000..45675e6b5 --- /dev/null +++ b/testdata/catalogs/test-catalog/catalog.yaml @@ -0,0 +1,42 @@ +--- +schema: olm.package +name: prometheus +defaultChannel: beta +--- +schema: olm.channel +name: beta +package: prometheus +entries: + - name: prometheus-operator.0.47.0 +--- +schema: olm.bundle +name: prometheus-operator.0.47.0 +package: prometheus +image: localhost/testdata/bundles/registry-v1/prometheus-operator:v0.47.0 +properties: + - type: olm.package + value: + packageName: prometheus + version: 0.47.0 +--- +schema: olm.package +name: plain +defaultChannel: beta +--- +schema: olm.channel +name: beta +package: plain +entries: + - name: plain.0.1.0 +--- +schema: olm.bundle +name: plain.0.1.0 +package: plain +image: localhost/testdata/bundles/plain-v0/plain:v0.1.0 +properties: + - type: olm.package + value: + packageName: plain + version: 0.1.0 + - type: olm.bundle.mediatype + value: plain+v0