diff --git a/Makefile b/Makefile index 848bd71a9..d7c6a7916 100644 --- a/Makefile +++ b/Makefile @@ -99,8 +99,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 @@ -195,7 +197,7 @@ CONTROLLER_TOOLS_VERSION ?= v0.10.0 .PHONY: kind kind: $(KIND) ## Download kind locally if necessary. $(KIND): $(LOCALBIN) - test -s $(LOCALBIN)/kind || GOBIN=$(LOCALBIN) go install sigs.k8s.io/kind@v0.15.0 + test -s $(LOCALBIN)/kind || GOBIN=$(LOCALBIN) go install sigs.k8s.io/kind@v0.19.0 .PHONY: ginkgo ginkgo: $(GINKGO) ## Download ginkgo locally if necessary. diff --git a/internal/controllers/operator_controller_test.go b/internal/controllers/operator_controller_test.go index d20fa5326..94bddab57 100644 --- a/internal/controllers/operator_controller_test.go +++ b/internal/controllers/operator_controller_test.go @@ -960,6 +960,108 @@ var _ = Describe("Operator Controller Test", func() { err := cl.Delete(ctx, operator) Expect(err).To(Not(HaveOccurred())) }) + 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 bade 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 ( @@ -1061,4 +1163,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 c74399b96..4bd5e9a0e 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" @@ -86,6 +87,8 @@ func getEntities(ctx context.Context, client client.Client) (input.EntityList, e // 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/test/e2e/install_test.go b/test/e2e/install_test.go index adde7e7c7..28506b043 100644 --- a/test/e2e/install_test.go +++ b/test/e2e/install_test.go @@ -17,7 +17,7 @@ import ( ) const ( - defaultTimeout = 30 * time.Second + defaultTimeout = 120 * time.Second defaultPoll = 1 * time.Second testCatalogRef = "localhost/testdata/catalogs/test-catalog:e2e" ) @@ -27,25 +27,17 @@ var _ = Describe("Operator Install", func() { ctx context.Context pkgName string operatorName string + catalogName string operator *operatorv1alpha1.Operator operatorCatalog *catalogd.Catalog ) 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, - }, - } + catalogName = fmt.Sprintf("catalog-%s", rand.String(8)) operatorCatalog = &catalogd.Catalog{ ObjectMeta: metav1.ObjectMeta{ - Name: "test-catalog", + Name: catalogName, }, Spec: catalogd.CatalogSpec{ Source: catalogd.CatalogSource{ @@ -59,54 +51,140 @@ 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: catalogName}, 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")) - }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) - }) - It("resolves the specified package with correct bundle path", func() { - 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()) - g.Expect(len(operator.Status.Conditions)).To(Equal(2)) - cond := apimeta.FindStatusCondition(operator.Status.Conditions, operatorv1alpha1.TypeResolved) + 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(operatorv1alpha1.ReasonSuccess)) - g.Expect(cond.Message).To(ContainSubstring("resolved to")) - g.Expect(operator.Status.ResolvedBundleResource).ToNot(BeEmpty()) - }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + g.Expect(cond.Message).To(ContainSubstring("successfully unpacked the catalog image")) - 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) + // For some reason the above condition check is returning true and the + // Operators end up being created before the packages exist. Adding this check + // to ensure that there are some packages that exist before actually returning from this + // check. + pkgList := &catalogd.PackageList{} + err = c.List(ctx, pkgList) 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")) + g.Expect(pkgList.Items).To(HaveLen(2)) }).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("the operator bundle format is registry+v1", func() { + BeforeEach(func() { + pkgName = "prometheus" + operatorName = fmt.Sprintf("operator-%s", rand.String(8)) + operator = &operatorv1alpha1.Operator{ + ObjectMeta: metav1.ObjectMeta{ + Name: operatorName, + }, + Spec: operatorv1alpha1.OperatorSpec{ + PackageName: pkgName, + }, + } + }) + It("resolves the specified package with correct bundle path", func() { + 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()) + 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()) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).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()) + 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")) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + + }) + AfterEach(func() { + err := c.Delete(ctx, operator) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + When("the operator bundle format is plain+v0", func() { + BeforeEach(func() { + pkgName = "plain" + operatorName = fmt.Sprintf("operator-%s", rand.String(8)) + operator = &operatorv1alpha1.Operator{ + ObjectMeta: metav1.ObjectMeta{ + Name: operatorName, + }, + Spec: operatorv1alpha1.OperatorSpec{ + PackageName: pkgName, + }, + } + }) + It("resolves the specified package with correct bundle path", func() { + 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()) + 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()) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).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()) + 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")) + }).WithTimeout(defaultTimeout).WithPolling(defaultPoll).Should(Succeed()) + + }) + AfterEach(func() { + err := c.Delete(ctx, operator) + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) }) diff --git a/testdata/catalogs/test-catalog.Dockerfile b/testdata/catalogs/test-catalog.Dockerfile index 0b46d7d46..ea1030964 100644 --- a/testdata/catalogs/test-catalog.Dockerfile +++ b/testdata/catalogs/test-catalog.Dockerfile @@ -1,15 +1,2 @@ -# The base image is expected to contain -# /bin/opm (with a serve subcommand) and /bin/grpc_health_probe -FROM quay.io/operator-framework/opm:latest - -# Configure the entrypoint and command -ENTRYPOINT ["/bin/opm"] -CMD ["serve", "/configs", "--cache-dir=/tmp/cache"] - -# Copy declarative config root into image at /configs and pre-populate serve cache +FROM scratch ADD test-catalog /configs -RUN ["/bin/opm", "serve", "/configs", "--cache-dir=/tmp/cache", "--cache-only"] - -# Set DC-specific label for the location of the DC root directory -# in the image -LABEL operators.operatorframework.io.index.configs.v1=/configs diff --git a/testdata/catalogs/test-catalog/catalog.yaml b/testdata/catalogs/test-catalog/catalog.yaml index df1bf940f..6ed13bd31 100644 --- a/testdata/catalogs/test-catalog/catalog.yaml +++ b/testdata/catalogs/test-catalog/catalog.yaml @@ -1,104 +1,48 @@ -{ - "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.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "Alertmanager", - "version": "v1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "AlertmanagerConfig", - "version": "v1alpha1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "PodMonitor", - "version": "v1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "Probe", - "version": "v1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "Prometheus", - "version": "v1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "PrometheusRule", - "version": "v1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "ServiceMonitor", - "version": "v1" - } - }, - { - "type": "olm.gvk", - "value": { - "group": "monitoring.coreos.com", - "kind": "ThanosRuler", - "version": "v1" - } - }, - { - "type": "olm.package", - "value": { - "packageName": "prometheus", - "version": "0.47.0" - } - } - ], - "relatedImages": [ - { - "name": "", - "image": "quay.io/operatorhubio/prometheus@sha256:47c659b352bbc4b75b64fbe0077d19afaea80cf691c62d803d44fdfc88dc9c6f" - }, - { - "name": "", - "image": "quay.io/prometheus-operator/prometheus-operator:v0.47.0" - } - ] -} +--- +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 +relatedImages: + - name: "" + image: "" +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 +relatedImages: + - name: "" + image: "" +properties: + - type: olm.package + value: + packageName: plain + version: 0.1.0 + - type: olm.bundle.mediatype + value: plain+v0