diff --git a/api/v1/clusterextension_types.go b/api/v1/clusterextension_types.go index e331ec63e..531d9bc38 100644 --- a/api/v1/clusterextension_types.go +++ b/api/v1/clusterextension_types.go @@ -108,16 +108,28 @@ type ClusterExtensionSpec struct { Config *ClusterExtensionConfig `json:"config,omitempty"` } -const SourceTypeCatalog = "Catalog" +const ( + SourceTypeBundle = "Bundle" + SourceTypeCatalog = "Catalog" +) // 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 a required reference to the type of install source. // - // Allowed values are "Catalog" + // + // Allowed values are "Bundle" or "Catalog" + // + // + // When this field is set to "Bundle", the bundle of content to install + // is specified directly. In this case, no interaction with ClusterCatalog + // resources is necessary. When using the Bundle sourceType, the bundle + // field must also be set. + // // // When this field is set to "Catalog", information for determining the // appropriate bundle of content to install will be fetched from @@ -125,10 +137,18 @@ type SourceConfig struct { // When using the Catalog sourceType, the catalog field must also be set. // // +unionDiscriminator - // +kubebuilder:validation:Enum:="Catalog" + // + // // +kubebuilder:validation:Required SourceType string `json:"sourceType"` + // bundle is used to configure how information is sourced from a bundle. + // This field is required when sourceType is "Bundle", and forbidden otherwise. + // + // +optional. + // + Bundle *BundleSource `json:"bundle,omitempty"` + // catalog is used to configure how information is sourced from a catalog. // This field is required when sourceType is "Catalog", and forbidden otherwise. // @@ -180,6 +200,60 @@ type ClusterExtensionConfig struct { Inline *apiextensionsv1.JSON `json:"inline,omitempty"` } +// BundleSource defines the configuration used to retrieve a bundle directly from +// its OCI-based image reference. +type BundleSource struct { + // ref allows users to define the reference to a container image containing bundle contents. + // ref is required. + // ref can not be more than 1000 characters. + // + // A reference can be broken down into 3 parts - the domain, name, and identifier. + // + // The domain is typically the registry where an image is located. + // It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + // Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + // Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + // The port must be the last value in the domain. + // Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + // + // The name is typically the repository in the registry where an image is located. + // It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + // Multiple names can be concatenated with the "/" character. + // The domain and name are combined using the "/" character. + // Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod". + // An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle". + // + // The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + // It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + // For a digest-based reference, the "@" character is the separator. + // For a tag-based reference, the ":" character is the separator. + // An identifier is required in the reference. + // + // Digest-based references must contain an algorithm reference immediately after the "@" separator. + // The algorithm reference must be followed by the ":" character and an encoded string. + // The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + // Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + // The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + // + // Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + // The tag must not be longer than 127 characters. + // + // An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + // An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + // + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength:=1000 + // +kubebuilder:validation:XValidation:rule="self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\\\b')",message="must start with a valid domain. valid domains must be alphanumeric characters (lowercase and uppercase) separated by the \".\" character." + // +kubebuilder:validation:XValidation:rule="self.find('(\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') != \"\"",message="a valid name is required. valid names must contain lowercase alphanumeric characters separated only by the \".\", \"_\", \"__\", \"-\" characters." + // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" || self.find(':.*$') != \"\"",message="must end with a digest or a tag" + // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') == \"\" ? (self.find(':.*$') != \"\" ? self.find(':.*$').substring(1).size() <= 127 : true) : true",message="tag is invalid. the tag must not be more than 127 characters" + // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') == \"\" ? (self.find(':.*$') != \"\" ? self.find(':.*$').matches(':[\\\\w][\\\\w.-]*$') : true) : true",message="tag is invalid. valid tags must begin with a word character (alphanumeric + \"_\") followed by word characters or \".\", and \"-\" characters" + // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find('(@.*:)').matches('(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])') : true",message="digest algorithm is not valid. valid algorithms must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the \"-\", \"_\", \"+\", and \".\" characters." + // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find(':.*$').substring(1).size() >= 32 : true",message="digest is not valid. the encoded string must be at least 32 characters" + // +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" ? self.find(':.*$').matches(':[0-9A-Fa-f]*$') : true",message="digest is not valid. the encoded string must only contain hex characters (A-F, a-f, 0-9)" + Ref string `json:"ref"` +} + // CatalogFilter defines the attributes used to identify and filter content from a catalog. type CatalogFilter struct { // packageName is a reference to the name of the package to be installed diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index e13f1532b..5494ae7a0 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -40,6 +40,21 @@ func (in *BundleMetadata) DeepCopy() *BundleMetadata { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BundleSource) DeepCopyInto(out *BundleSource) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BundleSource. +func (in *BundleSource) DeepCopy() *BundleSource { + if in == nil { + return nil + } + out := new(BundleSource) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CRDUpgradeSafetyPreflightConfig) DeepCopyInto(out *CRDUpgradeSafetyPreflightConfig) { *out = *in @@ -647,6 +662,11 @@ func (in *ServiceAccountReference) DeepCopy() *ServiceAccountReference { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SourceConfig) DeepCopyInto(out *SourceConfig) { *out = *in + if in.Bundle != nil { + in, out := &in.Bundle, &out.Bundle + *out = new(BundleSource) + **out = **in + } if in.Catalog != nil { in, out := &in.Catalog, &out.Catalog *out = new(CatalogFilter) diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 0ebce0f71..1536bc274 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -404,7 +404,8 @@ func run() error { return httputil.BuildHTTPClient(cpwCatalogd) }) - resolver := &resolve.CatalogResolver{ + resolver := &resolve.MultiResolver{} + resolver.RegisterType(ocv1.SourceTypeCatalog, &resolve.CatalogResolver{ WalkCatalogsFunc: resolve.CatalogWalker( func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) { var catalogs ocv1.ClusterCatalogList @@ -418,6 +419,12 @@ func run() error { Validations: []resolve.ValidationFunc{ resolve.NoDependencyValidation, }, + }) + if features.OperatorControllerFeatureGate.Enabled(features.DirectBundleInstall) { + resolver.RegisterType(ocv1.SourceTypeBundle, &resolve.BundleResolver{ + ImagePuller: imagePuller, + ImageCache: imageCache, + }) } aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) diff --git a/docs/api-reference/olmv1-api-reference.md b/docs/api-reference/olmv1-api-reference.md index 1b1ad6656..e066befad 100644 --- a/docs/api-reference/olmv1-api-reference.md +++ b/docs/api-reference/olmv1-api-reference.md @@ -50,6 +50,23 @@ _Appears in:_ | `version` _string_ | version is a required field and is a reference to the version that this bundle represents
version follows the semantic versioning standard as defined in https://semver.org/. | | Required: \{\}
| +#### BundleSource + + + +BundleSource defines the configuration used to retrieve a bundle directly from +its OCI-based image reference. + + + +_Appears in:_ +- [SourceConfig](#sourceconfig) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `ref` _string_ | ref allows users to define the reference to a container image containing bundle contents.
ref is required.
ref can not be more than 1000 characters.

A reference can be broken down into 3 parts - the domain, name, and identifier.

The domain is typically the registry where an image is located.
It must be alphanumeric characters (lowercase and uppercase) separated by the "." character.
Hyphenation is allowed, but the domain must start and end with alphanumeric characters.
Specifying a port to use is also allowed by adding the ":" character followed by numeric values.
The port must be the last value in the domain.
Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080".

The name is typically the repository in the registry where an image is located.
It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters.
Multiple names can be concatenated with the "/" character.
The domain and name are combined using the "/" character.
Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod".
An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle".

The identifier is typically the tag or digest for an image reference and is present at the end of the reference.
It starts with a separator character used to distinguish the end of the name and beginning of the identifier.
For a digest-based reference, the "@" character is the separator.
For a tag-based reference, the ":" character is the separator.
An identifier is required in the reference.

Digest-based references must contain an algorithm reference immediately after the "@" separator.
The algorithm reference must be followed by the ":" character and an encoded string.
The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters.
Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58".
The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters.

Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters.
The tag must not be longer than 127 characters.

An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"
An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" | | MaxLength: 1000
Required: \{\}
| + + #### CRDUpgradeSafetyEnforcement _Underlying type:_ _string_ @@ -459,13 +476,17 @@ _Appears in:_ SourceConfig is a discriminated union which selects the installation source. + + + _Appears in:_ - [ClusterExtensionSpec](#clusterextensionspec) | Field | Description | Default | Validation | | --- | --- | --- | --- | -| `sourceType` _string_ | sourceType is a required reference to the type of install source.

Allowed values are "Catalog"

When this field is set to "Catalog", information for determining the
appropriate bundle of content to install will be fetched from
ClusterCatalog resources existing on the cluster.
When using the Catalog sourceType, the catalog field must also be set. | | Enum: [Catalog]
Required: \{\}
| +| `sourceType` _string_ | sourceType is a required reference to the type of install source.

Allowed values are "Bundle" or "Catalog"


When this field is set to "Bundle", the bundle of content to install
is specified directly. In this case, no interaction with ClusterCatalog
resources is necessary. When using the Bundle sourceType, the bundle
field must also be set.


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


| | Required: \{\}
| +| `bundle` _[BundleSource](#bundlesource)_ | bundle is used to configure how information is sourced from a bundle.
This field is required when sourceType is "Bundle", and forbidden otherwise.

| | | | `catalog` _[CatalogFilter](#catalogfilter)_ | catalog is used to configure how information is sourced from a catalog.
This field is required when sourceType is "Catalog", and forbidden otherwise. | | | diff --git a/go.mod b/go.mod index 35254bcbf..320d9382f 100644 --- a/go.mod +++ b/go.mod @@ -126,6 +126,7 @@ require ( github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-migrate/migrate/v4 v4.19.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect diff --git a/hack/tools/crd-generator/main.go b/hack/tools/crd-generator/main.go index 9687489f4..932280a67 100644 --- a/hack/tools/crd-generator/main.go +++ b/hack/tools/crd-generator/main.go @@ -258,13 +258,13 @@ func formatDescription(description string, channel string, name string) string { startTag := "" endTag := "" if channel == StandardChannel && strings.Contains(description, startTag) { - regexPattern := `\n*` + regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) + `\n*` + regexPattern := regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) re := regexp.MustCompile(regexPattern) match := re.FindStringSubmatch(description) if len(match) != 2 { log.Fatalf("Invalid tag for %s", name) } - description = re.ReplaceAllString(description, "\n\n") + description = re.ReplaceAllString(description, "") } else { description = strings.ReplaceAll(description, startTag, "") description = strings.ReplaceAll(description, endTag, "") diff --git a/helm/experimental.yaml b/helm/experimental.yaml index fd7d9702e..df42afd0b 100644 --- a/helm/experimental.yaml +++ b/helm/experimental.yaml @@ -11,6 +11,7 @@ operatorControllerFeatures: - PreflightPermissions - HelmChartSupport - BoxcutterRuntime + - DirectBundleInstall # List of enabled experimental features for catalogd # Use with {{- if has "FeatureGate" .Values.catalogdFeatures }} 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 4cae796a6..d534a7374 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 @@ -225,6 +225,93 @@ spec: catalog: packageName: example-package properties: + bundle: + description: |- + bundle is used to configure how information is sourced from a bundle. + This field is required when sourceType is "Bundle", and forbidden otherwise. + properties: + ref: + description: |- + ref allows users to define the reference to a container image containing bundle contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object catalog: description: |- catalog is used to configure how information is sourced from a catalog. @@ -474,13 +561,19 @@ spec: description: |- sourceType is a required reference to the type of install source. - Allowed values are "Catalog" + Allowed values are "Bundle" or "Catalog" + + When this field is set to "Bundle", the bundle of content to install + is specified directly. In this case, no interaction with ClusterCatalog + resources is necessary. When using the Bundle sourceType, the bundle + field must also be set. When this field is set to "Catalog", information for determining the appropriate bundle of content to install will be fetched from ClusterCatalog resources existing on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: + - Bundle - Catalog type: string required: diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 1abdf0a18..230993b17 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -18,6 +18,7 @@ const ( WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA" HelmChartSupport featuregate.Feature = "HelmChartSupport" BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime" + DirectBundleInstall featuregate.Feature = "DirectBundleInstall" ) var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ @@ -80,6 +81,12 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature PreRelease: featuregate.Alpha, LockToDefault: false, }, + + DirectBundleInstall: { + Default: false, + PreRelease: featuregate.Alpha, + LockToDefault: false, + }, } var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate() diff --git a/internal/operator-controller/resolve/bundle.go b/internal/operator-controller/resolve/bundle.go new file mode 100644 index 000000000..10620caad --- /dev/null +++ b/internal/operator-controller/resolve/bundle.go @@ -0,0 +1,78 @@ +package resolve + +import ( + "context" + "errors" + "fmt" + "io/fs" + "reflect" + + bsemver "github.com/blang/semver/v4" + + "github.com/operator-framework/operator-registry/alpha/action" + "github.com/operator-framework/operator-registry/alpha/declcfg" + + 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/shared/util/image" +) + +type BundleResolver struct { + ImagePuller image.Puller + ImageCache image.Cache +} + +func (r *BundleResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + bundleFS, canonicalRef, _, err := r.ImagePuller.Pull(ctx, ext.Name, ext.Spec.Source.Bundle.Ref, r.ImageCache) + if err != nil { + return nil, nil, nil, err + } + + // TODO: This is a temporary workaround to get the bundle from the filesystem + // until the operator-registry library is updated to support reading from a + // fs.FS. This will be removed once the library is updated. + bundlePath, err := getDirFSPath(bundleFS) + if err != nil { + panic(fmt.Errorf("expected to be able to recover bundle path from bundleFS: %v", err)) + } + + // Render the bundle + render := action.Render{ + Refs: []string{bundlePath}, + AllowedRefMask: action.RefBundleDir, + } + fbc, err := render.Run(ctx) + if err != nil { + return nil, nil, nil, err + } + if len(fbc.Bundles) != 1 { + return nil, nil, nil, errors.New("expected exactly one bundle") + } + bundle := fbc.Bundles[0] + bundle.Image = canonicalRef.String() + v, err := bundleutil.GetVersion(bundle) + if err != nil { + return nil, nil, nil, err + } + return &bundle, v, nil, nil +} + +// A function to recover the underlying path string from os.DirFS +func getDirFSPath(f fs.FS) (string, error) { + v := reflect.ValueOf(f) + + // Check if the underlying type is a string (its kind) + if v.Kind() != reflect.String { + return "", fmt.Errorf("underlying type is not a string, it is %s", v.Kind()) + } + + // The type itself (os.dirFS) is unexported, but its Kind is a string. + // We can convert the reflect.Value back to a regular string using .Interface() + // after converting it to a basic string type. + path, ok := v.Convert(reflect.TypeOf("")).Interface().(string) + if !ok { + return "", fmt.Errorf("could not convert reflected value to string") + } + + return path, nil +} diff --git a/internal/operator-controller/resolve/resolver.go b/internal/operator-controller/resolve/resolver.go index 625111d63..06506a45d 100644 --- a/internal/operator-controller/resolve/resolver.go +++ b/internal/operator-controller/resolve/resolver.go @@ -2,6 +2,7 @@ package resolve import ( "context" + "fmt" bsemver "github.com/blang/semver/v4" @@ -19,3 +20,18 @@ type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { return f(ctx, ext, installedBundle) } + +type MultiResolver map[string]Resolver + +func (m MultiResolver) RegisterType(sourceType string, r Resolver) { + m[sourceType] = r +} + +func (m MultiResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + t := ext.Spec.Source.SourceType + r, ok := m[t] + if !ok { + return nil, nil, nil, fmt.Errorf("no resolver for source type %q", t) + } + return r.Resolve(ctx, ext, installedBundle) +} diff --git a/manifests/experimental-e2e.yaml b/manifests/experimental-e2e.yaml index 1bc93321e..34d2d332d 100644 --- a/manifests/experimental-e2e.yaml +++ b/manifests/experimental-e2e.yaml @@ -1023,6 +1023,93 @@ spec: catalog: packageName: example-package properties: + bundle: + description: |- + bundle is used to configure how information is sourced from a bundle. + This field is required when sourceType is "Bundle", and forbidden otherwise. + properties: + ref: + description: |- + ref allows users to define the reference to a container image containing bundle contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object catalog: description: |- catalog is used to configure how information is sourced from a catalog. @@ -1272,13 +1359,19 @@ spec: description: |- sourceType is a required reference to the type of install source. - Allowed values are "Catalog" + Allowed values are "Bundle" or "Catalog" + + When this field is set to "Bundle", the bundle of content to install + is specified directly. In this case, no interaction with ClusterCatalog + resources is necessary. When using the Bundle sourceType, the bundle + field must also be set. When this field is set to "Catalog", information for determining the appropriate bundle of content to install will be fetched from ClusterCatalog resources existing on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: + - Bundle - Catalog type: string required: @@ -2171,6 +2264,7 @@ spec: - --feature-gates=PreflightPermissions=true - --feature-gates=HelmChartSupport=true - --feature-gates=BoxcutterRuntime=true + - --feature-gates=DirectBundleInstall=true - --tls-cert=/var/certs/tls.crt - --tls-key=/var/certs/tls.key - --catalogd-cas-dir=/var/ca-certs diff --git a/manifests/experimental.yaml b/manifests/experimental.yaml index 69128a8b7..da58a3c12 100644 --- a/manifests/experimental.yaml +++ b/manifests/experimental.yaml @@ -988,6 +988,93 @@ spec: catalog: packageName: example-package properties: + bundle: + description: |- + bundle is used to configure how information is sourced from a bundle. + This field is required when sourceType is "Bundle", and forbidden otherwise. + properties: + ref: + description: |- + ref allows users to define the reference to a container image containing bundle contents. + ref is required. + ref can not be more than 1000 characters. + + A reference can be broken down into 3 parts - the domain, name, and identifier. + + The domain is typically the registry where an image is located. + It must be alphanumeric characters (lowercase and uppercase) separated by the "." character. + Hyphenation is allowed, but the domain must start and end with alphanumeric characters. + Specifying a port to use is also allowed by adding the ":" character followed by numeric values. + The port must be the last value in the domain. + Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080". + + The name is typically the repository in the registry where an image is located. + It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters. + Multiple names can be concatenated with the "/" character. + The domain and name are combined using the "/" character. + Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod". + An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle". + + The identifier is typically the tag or digest for an image reference and is present at the end of the reference. + It starts with a separator character used to distinguish the end of the name and beginning of the identifier. + For a digest-based reference, the "@" character is the separator. + For a tag-based reference, the ":" character is the separator. + An identifier is required in the reference. + + Digest-based references must contain an algorithm reference immediately after the "@" separator. + The algorithm reference must be followed by the ":" character and an encoded string. + The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters. + Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58". + The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters. + + Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters. + The tag must not be longer than 127 characters. + + An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05" + An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" + maxLength: 1000 + type: string + x-kubernetes-validations: + - message: must start with a valid domain. valid domains must + be alphanumeric characters (lowercase and uppercase) separated + by the "." character. + rule: self.matches('^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])((\\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(:[0-9]+)?\\b') + - message: a valid name is required. valid names must contain + lowercase alphanumeric characters separated only by the + ".", "_", "__", "-" characters. + rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)') + != "" + - message: must end with a digest or a tag + rule: self.find('(@.*:)') != "" || self.find(':.*$') != + "" + - message: tag is invalid. the tag must not be more than 127 + characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').substring(1).size() <= 127 + : true) : true' + - message: tag is invalid. valid tags must begin with a word + character (alphanumeric + "_") followed by word characters + or ".", and "-" characters + rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'') + != "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'') + : true) : true' + - message: digest algorithm is not valid. valid algorithms + must start with an uppercase or lowercase alpha character + followed by alphanumeric characters and may contain the + "-", "_", "+", and "." characters. + rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'') + : true' + - message: digest is not valid. the encoded string must be + at least 32 characters + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size() + >= 32 : true' + - message: digest is not valid. the encoded string must only + contain hex characters (A-F, a-f, 0-9) + rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'') + : true' + required: + - ref + type: object catalog: description: |- catalog is used to configure how information is sourced from a catalog. @@ -1237,13 +1324,19 @@ spec: description: |- sourceType is a required reference to the type of install source. - Allowed values are "Catalog" + Allowed values are "Bundle" or "Catalog" + + When this field is set to "Bundle", the bundle of content to install + is specified directly. In this case, no interaction with ClusterCatalog + resources is necessary. When using the Bundle sourceType, the bundle + field must also be set. When this field is set to "Catalog", information for determining the appropriate bundle of content to install will be fetched from ClusterCatalog resources existing on the cluster. When using the Catalog sourceType, the catalog field must also be set. enum: + - Bundle - Catalog type: string required: @@ -2084,6 +2177,7 @@ spec: - --feature-gates=PreflightPermissions=true - --feature-gates=HelmChartSupport=true - --feature-gates=BoxcutterRuntime=true + - --feature-gates=DirectBundleInstall=true - --tls-cert=/var/certs/tls.crt - --tls-key=/var/certs/tls.key - --catalogd-cas-dir=/var/ca-certs