diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index 6f7912ae9b..872b6572c6 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -79,7 +79,6 @@ type ClusterExtensionSpec struct { // source is required and selects the installation source of content for this ClusterExtension. // Set the sourceType field to perform the selection. // - // Catalog is currently the only implemented sourceType. // Setting sourceType to "Catalog" requires the catalog field to also be defined. // // Below is a minimal example of a source definition (in yaml): @@ -122,23 +121,41 @@ type ClusterExtensionSpec struct { ProgressDeadlineMinutes int32 `json:"progressDeadlineMinutes,omitempty"` } -const SourceTypeCatalog = "Catalog" +const ( + SourceTypeCatalog = "Catalog" + SourceTypeOCIImage = "OCIImage" +) // SourceConfig is a discriminated union which selects the installation source. // // +union // +kubebuilder:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'Catalog' ? has(self.catalog) : !has(self.catalog)",message="catalog is required when sourceType is Catalog, and forbidden otherwise" +// type SourceConfig struct { // sourceType is required and specifies the type of install source. // - // The only allowed value is "Catalog". + // + // The allowed value is "Catalog". + // + // When set to "Catalog", information for determining the appropriate bundle of content to install + // is fetched from ClusterCatalog resources on the cluster. + // When using the Catalog sourceType, the catalog field must also be set. + // + // + // + // The allowed values are "Catalog" and "OCIImage". + // + // When set to "OCIImage", the bundle image is used directly. Direct sources do not perform + // dependency resolution and are only supported by the Boxcutter runtime. // // When set to "Catalog", information for determining the appropriate bundle of content to install // is fetched from ClusterCatalog resources on the cluster. // When using the Catalog sourceType, the catalog field must also be set. + // // // +unionDiscriminator // +kubebuilder:validation:Enum:="Catalog" + // // +required SourceType string `json:"sourceType"` @@ -147,6 +164,26 @@ type SourceConfig struct { // // +optional Catalog *CatalogFilter `json:"catalog,omitempty"` + + // ociImage configures a bundle image to install directly. + // + // They do not provide catalog dependency resolution or upgrade safety. + // + // + // +optional + OCIImage OCIImageSource `json:"ociImage,omitzero"` +} + +// OCIImageSource identifies a bundle image to install directly from an OCI registry. +// +kubebuilder:validation:MinProperties:=1 +type OCIImageSource struct { + // ref is a Docker-style image reference with a tag or digest. + // + // +required + // +kubebuilder:validation:MaxLength:=1000 + // +kubebuilder:validation:MinLength:=1 + // +kubebuilder:validation:XValidation:rule="self.matches(\"^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:[0-9]+)?/[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*(:[A-Za-z0-9_][A-Za-z0-9_.-]{0,126}|@[A-Za-z][A-Za-z0-9+._-]*:[0-9A-Fa-f]{32,})$\")",message="must be a complete image reference with a valid repository and tag or digest" + Ref string `json:"ref,omitempty"` } // ClusterExtensionInstallConfig is a union which selects the clusterExtension installation config. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 6836216378..53310813b1 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -642,6 +642,21 @@ func (in *ImageSource) DeepCopy() *ImageSource { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIImageSource) DeepCopyInto(out *OCIImageSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIImageSource. +func (in *OCIImageSource) DeepCopy() *OCIImageSource { + if in == nil { + return nil + } + out := new(OCIImageSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ObjectSelector) DeepCopyInto(out *ObjectSelector) { *out = *in @@ -810,6 +825,7 @@ func (in *SourceConfig) DeepCopyInto(out *SourceConfig) { *out = new(CatalogFilter) (*in).DeepCopyInto(*out) } + out.OCIImage = in.OCIImage } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceConfig. diff --git a/applyconfigurations/api/v1/clusterextensionspec.go b/applyconfigurations/api/v1/clusterextensionspec.go index 47d810a74a..cf0c910a4c 100644 --- a/applyconfigurations/api/v1/clusterextensionspec.go +++ b/applyconfigurations/api/v1/clusterextensionspec.go @@ -43,7 +43,6 @@ type ClusterExtensionSpecApplyConfiguration struct { // source is required and selects the installation source of content for this ClusterExtension. // Set the sourceType field to perform the selection. // - // Catalog is currently the only implemented sourceType. // Setting sourceType to "Catalog" requires the catalog field to also be defined. // // Below is a minimal example of a source definition (in yaml): diff --git a/applyconfigurations/api/v1/ociimagesource.go b/applyconfigurations/api/v1/ociimagesource.go new file mode 100644 index 0000000000..11ee5f265d --- /dev/null +++ b/applyconfigurations/api/v1/ociimagesource.go @@ -0,0 +1,41 @@ +/* +Copyright 2022. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by controller-gen-v0.21. DO NOT EDIT. + +package v1 + +// OCIImageSourceApplyConfiguration represents a declarative configuration of the OCIImageSource type for use +// with apply. +// +// OCIImageSource identifies a bundle image to install directly from an OCI registry. +type OCIImageSourceApplyConfiguration struct { + // ref is a Docker-style image reference with a tag or digest. + Ref *string `json:"ref,omitempty"` +} + +// OCIImageSourceApplyConfiguration constructs a declarative configuration of the OCIImageSource type for use with +// apply. +func OCIImageSource() *OCIImageSourceApplyConfiguration { + return &OCIImageSourceApplyConfiguration{} +} + +// WithRef sets the Ref field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Ref field is set to the value of the last call. +func (b *OCIImageSourceApplyConfiguration) WithRef(value string) *OCIImageSourceApplyConfiguration { + b.Ref = &value + return b +} diff --git a/applyconfigurations/api/v1/sourceconfig.go b/applyconfigurations/api/v1/sourceconfig.go index 13221594a1..60db67f62d 100644 --- a/applyconfigurations/api/v1/sourceconfig.go +++ b/applyconfigurations/api/v1/sourceconfig.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.20. DO NOT EDIT. +// Code generated by controller-gen-v0.21. DO NOT EDIT. package v1 @@ -21,18 +21,41 @@ package v1 // with apply. // // SourceConfig is a discriminated union which selects the installation source. +// +// type SourceConfigApplyConfiguration struct { // sourceType is required and specifies the type of install source. // - // The only allowed value is "Catalog". + // + // The allowed value is "Catalog". + // + // When set to "Catalog", information for determining the appropriate bundle of content to install + // is fetched from ClusterCatalog resources on the cluster. + // When using the Catalog sourceType, the catalog field must also be set. + // + // + // + // The allowed values are "Catalog" and "OCIImage". + // + // When set to "OCIImage", the bundle image is used directly. Direct sources do not perform + // dependency resolution and are only supported by the Boxcutter runtime. // // When set to "Catalog", information for determining the appropriate bundle of content to install // is fetched from ClusterCatalog resources on the cluster. // When using the Catalog sourceType, the catalog field must also be set. + // + // + // SourceType *string `json:"sourceType,omitempty"` // catalog configures how information is sourced from a catalog. // It is required when sourceType is "Catalog", and forbidden otherwise. Catalog *CatalogFilterApplyConfiguration `json:"catalog,omitempty"` + // ociImage configures a bundle image to install directly. + // + // They do not provide catalog dependency resolution or upgrade safety. + // + // + OCIImage *OCIImageSourceApplyConfiguration `json:"ociImage,omitempty"` } // SourceConfigApplyConfiguration constructs a declarative configuration of the SourceConfig type for use with @@ -56,3 +79,11 @@ func (b *SourceConfigApplyConfiguration) WithCatalog(value *CatalogFilterApplyCo b.Catalog = value return b } + +// WithOCIImage sets the OCIImage field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the OCIImage field is set to the value of the last call. +func (b *SourceConfigApplyConfiguration) WithOCIImage(value *OCIImageSourceApplyConfiguration) *SourceConfigApplyConfiguration { + b.OCIImage = value + return b +} diff --git a/applyconfigurations/internal/internal.go b/applyconfigurations/internal/internal.go index dde5aaf513..d7bac0104e 100644 --- a/applyconfigurations/internal/internal.go +++ b/applyconfigurations/internal/internal.go @@ -381,6 +381,12 @@ var schemaYAML = typed.YAMLObject(`types: - name: ref type: scalar: string +- name: com.github.operator-framework.operator-controller.api.v1.OCIImageSource + map: + fields: + - name: ref + type: + scalar: string - name: com.github.operator-framework.operator-controller.api.v1.ObjectSelector map: fields: @@ -477,6 +483,9 @@ var schemaYAML = typed.YAMLObject(`types: - name: catalog type: namedType: com.github.operator-framework.operator-controller.api.v1.CatalogFilter + - name: ociImage + type: + namedType: com.github.operator-framework.operator-controller.api.v1.OCIImageSource - name: sourceType type: scalar: string diff --git a/applyconfigurations/utils.go b/applyconfigurations/utils.go index 6a467f96a6..6b09afb643 100644 --- a/applyconfigurations/utils.go +++ b/applyconfigurations/utils.go @@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -// Code generated by controller-gen-v0.20. DO NOT EDIT. +// Code generated by controller-gen-v0.21. DO NOT EDIT. package applyconfigurations @@ -85,6 +85,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} { return &apiv1.ObjectSourceRefApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ObservedPhase"): return &apiv1.ObservedPhaseApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("OCIImageSource"): + return &apiv1.OCIImageSourceApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("PreflightConfig"): return &apiv1.PreflightConfigApplyConfiguration{} case v1.SchemeGroupVersion.WithKind("ProgressionProbe"): diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 2fcea83ef0..eb8493cbfd 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -434,7 +434,7 @@ func run() error { return catalogclient.BuildHTTPClient(cpwCatalogd) }) - resolver := &resolve.CatalogResolver{ + catalogResolver := &resolve.CatalogResolver{ WalkCatalogsFunc: resolve.CatalogWalker( func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) { var catalogs ocv1.ClusterCatalogList @@ -449,6 +449,15 @@ func run() error { resolve.NoDependencyValidation, }, } + resolver := resolve.MultiResolver{ + ocv1.SourceTypeCatalog: catalogResolver, + } + if features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + resolver.RegisterType(ocv1.SourceTypeOCIImage, &resolve.OCIImageResolver{ + Puller: imagePuller, + Cache: imageCache, + }) + } aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) if err != nil { @@ -654,6 +663,8 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl controllers.HandleFinalizers(c.finalizers), controllers.ValidateClusterExtension( controllers.ServiceAccountDeprecationWarning(), + controllers.DirectBundleRequiresBoxcutter(), + controllers.ValidateDirectBundleSource(), ), controllers.MigrateStorage(storageMigrator), controllers.RetrieveRevisionStates(revisionStatesGetter), @@ -742,6 +753,8 @@ func (c *helmReconcilerConfigurator) Configure(ceReconciler *controllers.Cluster controllers.HandleFinalizers(c.finalizers), controllers.ValidateClusterExtension( controllers.ServiceAccountDeprecationWarning(), + controllers.DirectBundleRequiresBoxcutter(), + controllers.ValidateDirectBundleSource(), ), controllers.RetrieveRevisionStates(revisionStatesGetter), controllers.ResolveBundle(c.resolver, c.mgr.GetClient()), diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 1d686238ca..ff676965fb 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -360,7 +360,7 @@ _Appears in:_ | --- | --- | --- | --- | | `namespace` _string_ | namespace specifies a Kubernetes namespace.
It designates the default namespace where namespace-scoped resources for the extension are applied to the cluster.
Some extensions may contain namespace-scoped resources to be applied in other namespaces.
This namespace must exist.
The namespace field is required, immutable, and follows the DNS label standard as defined in [RFC 1123].
It must contain only lowercase alphanumeric characters or hyphens (-), start and end with an alphanumeric character,
and be no longer than 63 characters.
[RFC 1123]: https://tools.ietf.org/html/rfc1123 | | MaxLength: 63
Required: \{\}
| | `serviceAccount` _[ServiceAccountReference](#serviceaccountreference)_ | serviceAccount is a deprecated field and is completely ignored.
OLMv1 is a single-tenant system where users with ClusterExtension write access are
effectively delegated cluster-admin trust. The operator-controller runs with
cluster-admin privileges and uses its own service account for all cluster interactions.
Deprecated: serviceAccount is no longer used and will be removed in a future release. | | MinProperties: 1
Optional: \{\}
| -| `source` _[SourceConfig](#sourceconfig)_ | source is required and selects the installation source of content for this ClusterExtension.
Set the sourceType field to perform the selection.
Catalog is currently the only implemented sourceType.
Setting sourceType to "Catalog" requires the catalog field to also be defined.
Below is a minimal example of a source definition (in yaml):
source:
sourceType: Catalog
catalog:
packageName: example-package | | Required: \{\}
| +| `source` _[SourceConfig](#sourceconfig)_ | source is required and selects the installation source of content for this ClusterExtension.
Set the sourceType field to perform the selection.
Setting sourceType to "Catalog" requires the catalog field to also be defined.
Below is a minimal example of a source definition (in yaml):
source:
sourceType: Catalog
catalog:
packageName: example-package | | Required: \{\}
| | `install` _[ClusterExtensionInstallConfig](#clusterextensioninstallconfig)_ | install is optional and configures installation options for the ClusterExtension,
such as the pre-flight check configuration. | | Optional: \{\}
| | `config` _[ClusterExtensionConfig](#clusterextensionconfig)_ | config is optional and specifies bundle-specific configuration.
Configuration is bundle-specific and a bundle may provide a configuration schema.
When not specified, the default configuration of the resolved bundle is used.
config is validated against a configuration schema provided by the resolved bundle. If the bundle does not provide
a configuration schema the bundle is deemed to not be configurable. More information on how
to configure bundles can be found in the OLM documentation associated with your current OLM version.
| | Optional: \{\}
| | `progressDeadlineMinutes` _integer_ | progressDeadlineMinutes is an optional field that defines the maximum period
of time in minutes after which an installation should be considered failed and
require manual intervention. This functionality is disabled when no value
is provided. The minimum period is 10 minutes, and the maximum is 720 minutes (12 hours).
| | Maximum: 720
Minimum: 10
Optional: \{\}
| @@ -457,6 +457,23 @@ _Appears in:_ | `pollIntervalMinutes` _integer_ | pollIntervalMinutes is an optional field that sets the interval, in minutes, at which the image source is polled for new content.
You cannot specify pollIntervalMinutes when ref is a digest-based reference.
When omitted, the image is not polled for new content. | | Minimum: 1
Optional: \{\}
| +#### OCIImageSource + + + +OCIImageSource identifies a bundle image to install directly from an OCI registry. + +_Validation:_ +- MinProperties: 1 + +_Appears in:_ +- [SourceConfig](#sourceconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `ref` _string_ | ref is a Docker-style image reference with a tag or digest. | | MaxLength: 1000
MinLength: 1
Required: \{\}
| + + #### ObjectSelector @@ -606,6 +623,8 @@ _Appears in:_ SourceConfig is a discriminated union which selects the installation source. + + _Appears in:_ @@ -613,8 +632,9 @@ _Appears in:_ | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `sourceType` _string_ | sourceType is required and specifies the type of install source.
The only allowed value is "Catalog".
When set to "Catalog", information for determining the appropriate bundle of content to install
is fetched from ClusterCatalog resources on the cluster.
When using the Catalog sourceType, the catalog field must also be set. | | Enum: [Catalog]
Required: \{\}
| +| `sourceType` _string_ | sourceType is required and specifies the type of install source.

The allowed value is "Catalog".
When set to "Catalog", information for determining the appropriate bundle of content to install
is fetched from ClusterCatalog resources on the cluster.
When using the Catalog sourceType, the catalog field must also be set.


The allowed values are "Catalog" and "OCIImage".
When set to "OCIImage", the bundle image is used directly. Direct sources do not perform
dependency resolution and are only supported by the Boxcutter runtime.
When set to "Catalog", information for determining the appropriate bundle of content to install
is fetched from ClusterCatalog resources on the cluster.
When using the Catalog sourceType, the catalog field must also be set.

| | Enum: [Catalog]
Required: \{\}
| | `catalog` _[CatalogFilter](#catalogfilter)_ | catalog configures how information is sourced from a catalog.
It is required when sourceType is "Catalog", and forbidden otherwise. | | Optional: \{\}
| +| `ociImage` _[OCIImageSource](#ociimagesource)_ | ociImage configures a bundle image to install directly.

They do not provide catalog dependency resolution or upgrade safety.

| | MinProperties: 1
Optional: \{\}
| #### SourceType diff --git a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml index 3082a69946..4c55364fe9 100644 --- a/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml +++ b/helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml @@ -223,7 +223,6 @@ spec: source is required and selects the installation source of content for this ClusterExtension. Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType. Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -472,17 +471,41 @@ spec: required: - packageName type: object + ociImage: + description: |- + ociImage configures a bundle image to install directly. + + They do not provide catalog dependency resolution or upgrade safety. + minProperties: 1 + properties: + ref: + description: ref is a Docker-style image reference with a + tag or digest. + maxLength: 1000 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must be a complete image reference with a valid + repository and tag or digest + rule: self.matches("^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:[0-9]+)?/[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*(:[A-Za-z0-9_][A-Za-z0-9_.-]{0,126}|@[A-Za-z][A-Za-z0-9+._-]*:[0-9A-Fa-f]{32,})$") + required: + - ref + type: object sourceType: description: |- sourceType is required and specifies the type of install source. - The only allowed value is "Catalog". + The allowed values are "Catalog" and "OCIImage". + + When set to "OCIImage", the bundle image is used directly. Direct sources do not perform + dependency resolution and are only supported by the Boxcutter runtime. When set to "Catalog", information for determining the appropriate bundle of content to install is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog + - OCIImage type: string required: - sourceType diff --git a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml index 954dea621e..b1da1bd718 100644 --- a/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml +++ b/helm/olmv1/base/operator-controller/crd/standard/olm.operatorframework.io_clusterextensions.yaml @@ -175,7 +175,6 @@ spec: source is required and selects the installation source of content for this ClusterExtension. Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType. Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -428,7 +427,7 @@ spec: description: |- sourceType is required and specifies the type of install source. - The only allowed value is "Catalog". + The allowed value is "Catalog". When set to "Catalog", information for determining the appropriate bundle of content to install is fetched from ClusterCatalog resources on the cluster. diff --git a/internal/operator-controller/controllers/clusterextension_admission_test.go b/internal/operator-controller/controllers/clusterextension_admission_test.go index 14cfea8fc9..4fda708635 100644 --- a/internal/operator-controller/controllers/clusterextension_admission_test.go +++ b/internal/operator-controller/controllers/clusterextension_admission_test.go @@ -74,6 +74,62 @@ func TestClusterExtensionSourceConfig(t *testing.T) { } } +func TestClusterExtensionOCIImageSourceConfig(t *testing.T) { + t.Parallel() + testCases := []struct { + name string + source ocv1.SourceConfig + wantError bool + }{ + { + name: "valid tagged image", + source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: "quay.io/example/operator:latest"}, + }, + }, + { + name: "valid tagged image with registry port", + source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: "quay.io:5000/example/operator:latest"}, + }, + }, + { + name: "valid digested image with registry port", + source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: "quay.io:5000/example/operator@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, + }, + }, + { + name: "uppercase repository segment", + source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: "quay.io/example/Operator:latest"}, + }, + wantError: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + cl := newClient(t) + err := cl.Create(context.Background(), buildClusterExtension(ocv1.ClusterExtensionSpec{ + Source: tc.source, + Namespace: "default", + })) + if tc.wantError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + func TestClusterExtensionAdmissionPackageName(t *testing.T) { tooLongError := "spec.source.catalog.packageName: Too long: may not be more than 253" regexMismatchError := "packageName must be a valid DNS1123 subdomain" diff --git a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go index b07a5072f4..e3e98c5f26 100644 --- a/internal/operator-controller/controllers/clusterextension_reconcile_steps.go +++ b/internal/operator-controller/controllers/clusterextension_reconcile_steps.go @@ -30,6 +30,7 @@ import ( ocv1 "github.com/operator-framework/operator-controller/api/v1" "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" "github.com/operator-framework/operator-controller/internal/operator-controller/labels" "github.com/operator-framework/operator-controller/internal/operator-controller/resolve" imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" @@ -108,6 +109,35 @@ func ServiceAccountDeprecationWarning() ClusterExtensionValidator { } } +// DirectBundleRequiresBoxcutter rejects direct OCI image sources when the +// Boxcutter runtime is unavailable. The Helm runtime has no direct-source +// implementation and must never silently interpret the source as a catalog. +func DirectBundleRequiresBoxcutter() ClusterExtensionValidator { + return func(_ context.Context, ext *ocv1.ClusterExtension) error { + if ext.Spec.Source.SourceType == ocv1.SourceTypeOCIImage && !features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) { + return fmt.Errorf("sourceType %q requires the %s feature gate", ocv1.SourceTypeOCIImage, features.BoxcutterRuntime) + } + return nil + } +} + +// ValidateDirectBundleSource validates the direct source fields that cannot be +// expressed in the standard CRD because OCIImage is experimental-only. +func ValidateDirectBundleSource() ClusterExtensionValidator { + return func(_ context.Context, ext *ocv1.ClusterExtension) error { + if ext.Spec.Source.SourceType != ocv1.SourceTypeOCIImage { + return nil + } + if ext.Spec.Source.OCIImage.Ref == "" { + return fmt.Errorf("sourceType %q requires ociImage.ref", ocv1.SourceTypeOCIImage) + } + if ext.Spec.Source.Catalog != nil { + return fmt.Errorf("sourceType %q forbids source.catalog", ocv1.SourceTypeOCIImage) + } + return nil + } +} + func RetrieveRevisionStates(r RevisionStatesGetter) ReconcileStepFunc { return func(ctx context.Context, state *reconcileState, ext *ocv1.ClusterExtension) (*ctrl.Result, error) { l := log.FromContext(ctx) @@ -137,16 +167,11 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc { // If already rolling out, use existing revision and set deprecation to Unknown (no catalog check) if len(state.revisionStates.RollingOut) > 0 { - installedBundleName := "" - if state.revisionStates.Installed != nil { - installedBundleName = state.revisionStates.Installed.Name - } - SetDeprecationStatus(ext, installedBundleName, nil, false) + SetDeprecationStatus(ext, installedBundleName(state.revisionStates), nil, false) state.resolvedRevisionMetadata = state.revisionStates.RollingOut[0] return nil, nil } - // Resolve a new bundle from the catalog l.V(1).Info("resolving bundle") var bm *ocv1.BundleMetadata if state.revisionStates.Installed != nil { @@ -156,10 +181,7 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc { // Get the installed bundle name for deprecation status. // BundleDeprecated should reflect what's currently running, not what we're trying to install. - installedBundleName := "" - if state.revisionStates.Installed != nil { - installedBundleName = state.revisionStates.Installed.Name - } + installedBundleName := installedBundleName(state.revisionStates) // Set deprecation status based on resolution results: // - If resolution succeeds: hasCatalogData=true, deprecation shows catalog data (nil=not deprecated) @@ -175,11 +197,19 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc { // the deprecation status to unknown? Or perhaps we somehow combine the deprecation information from // all catalogs? This needs a follow-up discussion and PR. hasCatalogData := err == nil || resolvedDeprecation != nil + if behavior, ok := r.(resolve.ResolverBehavior); ok { + hasCatalogData = behavior.HasCatalogData(ext) + } state.resolvedDeprecation = resolvedDeprecation state.hasCatalogData = hasCatalogData SetDeprecationStatus(ext, installedBundleName, resolvedDeprecation, hasCatalogData) if err != nil { + if behavior, ok := r.(resolve.ResolverBehavior); ok && !behavior.ShouldFallbackOnError(ext) { + setStatusProgressing(ext, err) + setInstalledStatusFromRevisionStates(ext, state.revisionStates) + return nil, err + } return handleResolutionError(ctx, c, state, ext, err) } @@ -198,6 +228,13 @@ func ResolveBundle(r resolve.Resolver, c client.Client) ReconcileStepFunc { } } +func installedBundleName(states *RevisionStates) string { + if states != nil && states.Installed != nil { + return states.Installed.Name + } + return "" +} + // handleResolutionError handles the case when bundle resolution fails. // // Decision logic (evaluated in order): diff --git a/internal/operator-controller/controllers/direct_bundle_test.go b/internal/operator-controller/controllers/direct_bundle_test.go new file mode 100644 index 0000000000..2f56149f44 --- /dev/null +++ b/internal/operator-controller/controllers/direct_bundle_test.go @@ -0,0 +1,52 @@ +package controllers + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/features" +) + +func TestDirectBundleRequiresBoxcutter(t *testing.T) { + previous := features.OperatorControllerFeatureGate.Enabled(features.BoxcutterRuntime) + t.Cleanup(func() { + _ = features.OperatorControllerFeatureGate.Set(string(features.BoxcutterRuntime) + "=" + boolString(previous)) + }) + + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Source: ocv1.SourceConfig{SourceType: ocv1.SourceTypeOCIImage}}} + validator := DirectBundleRequiresBoxcutter() + + require.NoError(t, features.OperatorControllerFeatureGate.Set(string(features.BoxcutterRuntime)+"=false")) + require.Error(t, validator(context.Background(), ext)) + + require.NoError(t, features.OperatorControllerFeatureGate.Set(string(features.BoxcutterRuntime)+"=true")) + require.NoError(t, validator(context.Background(), ext)) +} + +func TestValidateDirectBundleSource(t *testing.T) { + validator := ValidateDirectBundleSource() + + t.Run("requires reference", func(t *testing.T) { + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Source: ocv1.SourceConfig{SourceType: ocv1.SourceTypeOCIImage}}} + require.Error(t, validator(context.Background(), ext)) + }) + + t.Run("rejects catalog payload", func(t *testing.T) { + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: "quay.io/example/operator:latest"}, + Catalog: &ocv1.CatalogFilter{PackageName: "example"}, + }}} + require.Error(t, validator(context.Background(), ext)) + }) +} + +func boolString(value bool) string { + if value { + return "true" + } + return "false" +} diff --git a/internal/operator-controller/resolve/ociimage.go b/internal/operator-controller/resolve/ociimage.go new file mode 100644 index 0000000000..015e2d962f --- /dev/null +++ b/internal/operator-controller/resolve/ociimage.go @@ -0,0 +1,105 @@ +package resolve + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/operator-framework/operator-registry/alpha/declcfg" + "github.com/operator-framework/operator-registry/alpha/property" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" + bundlesource "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" +) + +// OCIImageResolver resolves a bundle directly from an OCI image. The image is +// unpacked through the shared image cache before its content is inspected. +type OCIImageResolver struct { + Puller imageutil.Puller + Cache imageutil.Cache +} + +// Resolve loads a registry+v1 bundle from the direct OCIImage source. Direct +// sources intentionally do not consult catalogs or perform dependency resolution. +func (r *OCIImageResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *declcfg.VersionRelease, *declcfg.Deprecation, error) { + if ext.Spec.Source.OCIImage.Ref == "" { + return nil, nil, nil, reconcile.TerminalError(fmt.Errorf("OCIImage source is missing ociImage.ref")) + } + if r.Puller == nil || r.Cache == nil { + return nil, nil, nil, fmt.Errorf("direct OCIImage resolver is not configured") + } + + imageFS, canonicalRef, _, err := r.Puller.Pull(ctx, ext.Name, ext.Spec.Source.OCIImage.Ref, r.Cache) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to pull direct bundle image: %w", err) + } + if canonicalRef == nil { + return nil, nil, nil, fmt.Errorf("direct bundle image pull returned no canonical reference") + } + + bundle, err := bundleFromFS(imageFS, canonicalRef.String()) + if err != nil { + return nil, nil, nil, reconcile.TerminalError(fmt.Errorf("invalid direct bundle image: %w", err)) + } + versionRelease, err := bundleutil.GetVersionAndRelease(*bundle) + if err != nil { + return nil, nil, nil, reconcile.TerminalError(err) + } + return bundle, versionRelease, nil, nil +} + +func bundleFromFS(bundleFS fs.FS, image string) (*declcfg.Bundle, error) { + registryBundle, err := bundlesource.FromFS(bundleFS).GetBundle() + if err != nil { + return nil, err + } + + bundle := &declcfg.Bundle{ + Name: registryBundle.CSV.Name, + Package: registryBundle.PackageName, + Image: image, + } + propertiesJSON := registryBundle.CSV.Annotations[bundlesource.PropertyOLMProperties] + if propertiesJSON == "" { + return nil, fmt.Errorf("bundle %q has no %q package property", bundle.Name, bundlesource.PropertyOLMProperties) + } + if err := json.Unmarshal([]byte(propertiesJSON), &bundle.Properties); err != nil { + return nil, fmt.Errorf("failed to parse bundle properties: %w", err) + } + if err := validatePackageProperty(bundle.Properties, registryBundle.PackageName); err != nil { + return nil, err + } + return bundle, nil +} + +func validatePackageProperty(properties []property.Property, expectedPackageName string) error { + var packageProperties []property.Property + for _, p := range properties { + if p.Type == property.TypePackage { + packageProperties = append(packageProperties, p) + } + } + if len(packageProperties) != 1 { + return fmt.Errorf("expected exactly one %q package property, found %d", property.TypePackage, len(packageProperties)) + } + + var packageData struct { + PackageName string `json:"packageName"` + Version string `json:"version"` + } + if err := json.Unmarshal(packageProperties[0].Value, &packageData); err != nil { + return fmt.Errorf("failed to parse %q package property: %w", property.TypePackage, err) + } + if packageData.PackageName == "" || packageData.PackageName != expectedPackageName { + return fmt.Errorf("package property name %q does not match bundle package name %q", packageData.PackageName, expectedPackageName) + } + if packageData.Version == "" { + return fmt.Errorf("package property for %q has no version", expectedPackageName) + } + return nil +} diff --git a/internal/operator-controller/resolve/ociimage_test.go b/internal/operator-controller/resolve/ociimage_test.go new file mode 100644 index 0000000000..1f2770bb90 --- /dev/null +++ b/internal/operator-controller/resolve/ociimage_test.go @@ -0,0 +1,89 @@ +package resolve + +import ( + "context" + "io/fs" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.podman.io/image/v5/docker/reference" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source" + imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image" + csvbuilder "github.com/operator-framework/operator-controller/internal/testing/bundle/csv" + bundlefs "github.com/operator-framework/operator-controller/internal/testing/bundle/fs" +) + +func TestOCIImageResolverResolve(t *testing.T) { + ref := "quay.io/example/operator@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + bundleFS := bundlefs.Builder(). + WithPackageName("example-operator"). + WithCSV(csvbuilder.Builder().WithName("example-operator.v1.2.3").WithAnnotations(map[string]string{ + source.PropertyOLMProperties: `[{"type":"olm.package","value":{"packageName":"example-operator","version":"1.2.3"}}]`, + }).Build()). + Build() + + resolver := &OCIImageResolver{Puller: fakePuller{fs: bundleFS, ref: ref}, Cache: fakeCache{}} + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: ref}, + }}} + + bundle, version, deprecation, err := resolver.Resolve(context.Background(), ext, nil) + require.NoError(t, err) + require.Equal(t, "example-operator.v1.2.3", bundle.Name) + require.Equal(t, "example-operator", bundle.Package) + require.Equal(t, ref, bundle.Image) + require.Equal(t, "1.2.3", version.Version.String()) + require.Nil(t, deprecation) +} + +func TestOCIImageResolverRejectsInvalidBundle(t *testing.T) { + ref := "quay.io/example/operator@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + resolver := &OCIImageResolver{Puller: fakePuller{fs: bundlefs.Builder().Build(), ref: ref}, Cache: fakeCache{}} + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: ref}, + }}} + + _, _, _, err := resolver.Resolve(context.Background(), ext, nil) + require.Error(t, err) + require.ErrorIs(t, err, reconcile.TerminalError(nil)) +} + +func TestOCIImageResolverRejectsMismatchedPackageProperty(t *testing.T) { + ref := "quay.io/example/operator@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + bundleFS := bundlefs.Builder(). + WithPackageName("example-operator"). + WithCSV(csvbuilder.Builder().WithName("example-operator.v1.2.3").WithAnnotations(map[string]string{ + source.PropertyOLMProperties: `[{"type":"olm.package","value":{"packageName":"other-operator","version":"1.2.3"}}]`, + }).Build()). + Build() + resolver := &OCIImageResolver{Puller: fakePuller{fs: bundleFS, ref: ref}, Cache: fakeCache{}} + ext := &ocv1.ClusterExtension{Spec: ocv1.ClusterExtensionSpec{Source: ocv1.SourceConfig{ + SourceType: ocv1.SourceTypeOCIImage, + OCIImage: ocv1.OCIImageSource{Ref: ref}, + }}} + + _, _, _, err := resolver.Resolve(context.Background(), ext, nil) + require.ErrorIs(t, err, reconcile.TerminalError(nil)) + require.ErrorContains(t, err, "does not match bundle package name") +} + +type fakePuller struct { + fs fs.FS + ref string +} + +func (p fakePuller) Pull(context.Context, string, string, imageutil.Cache) (fs.FS, reference.Canonical, time.Time, error) { + canonical, err := reference.ParseNormalizedNamed(p.ref) + if err != nil { + return nil, nil, time.Time{}, err + } + return p.fs, canonical.(reference.Canonical), time.Time{}, nil +} + +type fakeCache struct{ imageutil.Cache } diff --git a/internal/operator-controller/resolve/resolver.go b/internal/operator-controller/resolve/resolver.go index ef7543b5c8..aac42615f8 100644 --- a/internal/operator-controller/resolve/resolver.go +++ b/internal/operator-controller/resolve/resolver.go @@ -2,6 +2,7 @@ package resolve import ( "context" + "fmt" "github.com/operator-framework/operator-registry/alpha/declcfg" @@ -12,8 +13,42 @@ type Resolver interface { Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *declcfg.VersionRelease, *declcfg.Deprecation, error) } +// ResolverBehavior describes source-specific reconciliation behavior that +// cannot be inferred from a resolved bundle alone. +type ResolverBehavior interface { + HasCatalogData(*ocv1.ClusterExtension) bool + ShouldFallbackOnError(*ocv1.ClusterExtension) bool +} + type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *declcfg.VersionRelease, *declcfg.Deprecation, error) func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *declcfg.VersionRelease, *declcfg.Deprecation, error) { return f(ctx, ext, installedBundle) } + +// MultiResolver dispatches bundle resolution by ClusterExtension source type. +type MultiResolver map[string]Resolver + +// RegisterType associates a source type with its resolver. +func (m MultiResolver) RegisterType(sourceType string, resolver Resolver) { + m[sourceType] = resolver +} + +// Resolve dispatches to the resolver selected by the ClusterExtension source type. +func (m MultiResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *declcfg.VersionRelease, *declcfg.Deprecation, error) { + resolver, ok := m[ext.Spec.Source.SourceType] + if !ok { + return nil, nil, nil, fmt.Errorf("no resolver for source type %q", ext.Spec.Source.SourceType) + } + return resolver.Resolve(ctx, ext, installedBundle) +} + +// HasCatalogData reports whether the selected source can provide catalog metadata. +func (m MultiResolver) HasCatalogData(ext *ocv1.ClusterExtension) bool { + return ext.Spec.Source.SourceType == ocv1.SourceTypeCatalog +} + +// ShouldFallbackOnError reports whether catalog fallback behavior is valid for the source. +func (m MultiResolver) ShouldFallbackOnError(ext *ocv1.ClusterExtension) bool { + return ext.Spec.Source.SourceType == ocv1.SourceTypeCatalog +} diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 6d9346b4ae..6f7dc0bfd6 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -837,7 +837,6 @@ spec: source is required and selects the installation source of content for this ClusterExtension. Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType. Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -1086,17 +1085,41 @@ spec: required: - packageName type: object + ociImage: + description: |- + ociImage configures a bundle image to install directly. + + They do not provide catalog dependency resolution or upgrade safety. + minProperties: 1 + properties: + ref: + description: ref is a Docker-style image reference with a + tag or digest. + maxLength: 1000 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must be a complete image reference with a valid + repository and tag or digest + rule: self.matches("^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:[0-9]+)?/[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*(:[A-Za-z0-9_][A-Za-z0-9_.-]{0,126}|@[A-Za-z][A-Za-z0-9+._-]*:[0-9A-Fa-f]{32,})$") + required: + - ref + type: object sourceType: description: |- sourceType is required and specifies the type of install source. - The only allowed value is "Catalog". + The allowed values are "Catalog" and "OCIImage". + + When set to "OCIImage", the bundle image is used directly. Direct sources do not perform + dependency resolution and are only supported by the Boxcutter runtime. When set to "Catalog", information for determining the appropriate bundle of content to install is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog + - OCIImage type: string required: - sourceType diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index f8c3add53b..2d4d858a5d 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -798,7 +798,6 @@ spec: source is required and selects the installation source of content for this ClusterExtension. Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType. Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -1047,17 +1046,41 @@ spec: required: - packageName type: object + ociImage: + description: |- + ociImage configures a bundle image to install directly. + + They do not provide catalog dependency resolution or upgrade safety. + minProperties: 1 + properties: + ref: + description: ref is a Docker-style image reference with a + tag or digest. + maxLength: 1000 + minLength: 1 + type: string + x-kubernetes-validations: + - message: must be a complete image reference with a valid + repository and tag or digest + rule: self.matches("^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:[0-9]+)?/[a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*(:[A-Za-z0-9_][A-Za-z0-9_.-]{0,126}|@[A-Za-z][A-Za-z0-9+._-]*:[0-9A-Fa-f]{32,})$") + required: + - ref + type: object sourceType: description: |- sourceType is required and specifies the type of install source. - The only allowed value is "Catalog". + The allowed values are "Catalog" and "OCIImage". + + When set to "OCIImage", the bundle image is used directly. Direct sources do not perform + dependency resolution and are only supported by the Boxcutter runtime. When set to "Catalog", information for determining the appropriate bundle of content to install is fetched from ClusterCatalog resources on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: - Catalog + - OCIImage type: string required: - sourceType diff --git a/manifests/standard-e2e.yaml b/manifests/standard-e2e.yaml index 28dca6563d..49e4f72300 100644 --- a/manifests/standard-e2e.yaml +++ b/manifests/standard-e2e.yaml @@ -789,7 +789,6 @@ spec: source is required and selects the installation source of content for this ClusterExtension. Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType. Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -1042,7 +1041,7 @@ spec: description: |- sourceType is required and specifies the type of install source. - The only allowed value is "Catalog". + The allowed value is "Catalog". When set to "Catalog", information for determining the appropriate bundle of content to install is fetched from ClusterCatalog resources on the cluster. diff --git a/manifests/standard.yaml b/manifests/standard.yaml index 71c7677772..a2576644e6 100644 --- a/manifests/standard.yaml +++ b/manifests/standard.yaml @@ -750,7 +750,6 @@ spec: source is required and selects the installation source of content for this ClusterExtension. Set the sourceType field to perform the selection. - Catalog is currently the only implemented sourceType. Setting sourceType to "Catalog" requires the catalog field to also be defined. Below is a minimal example of a source definition (in yaml): @@ -1003,7 +1002,7 @@ spec: description: |- sourceType is required and specifies the type of install source. - The only allowed value is "Catalog". + The allowed value is "Catalog". When set to "Catalog", information for determining the appropriate bundle of content to install is fetched from ClusterCatalog resources on the cluster.