Skip to content

Commit e799af7

Browse files
committed
Add experimental, feature-gated support for resolving bundles directly from their image reference
Signed-off-by: Joe Lanford <joe.lanford@gmail.com>
1 parent 029484d commit e799af7

File tree

13 files changed

+516
-10
lines changed

13 files changed

+516
-10
lines changed

api/v1/clusterextension_types.go

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,27 +108,47 @@ type ClusterExtensionSpec struct {
108108
Config *ClusterExtensionConfig `json:"config,omitempty"`
109109
}
110110

111-
const SourceTypeCatalog = "Catalog"
111+
const (
112+
SourceTypeBundle = "Bundle"
113+
SourceTypeCatalog = "Catalog"
114+
)
112115

113116
// SourceConfig is a discriminated union which selects the installation source.
114117
//
115118
// +union
119+
// <opcon:experimental:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'Bundle' ? has(self.bundle) : !has(self.bundle)",message="bundle is required when sourceType is Bundle, and forbidden otherwise">
116120
// +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"
117121
type SourceConfig struct {
118122
// sourceType is a required reference to the type of install source.
119123
//
120-
// Allowed values are "Catalog"
124+
//
125+
// Allowed values are <opcon:experimental:description>"Bundle" or </opcon:experimental:description>"Catalog"
126+
//
127+
// <opcon:experimental:description>
128+
// When this field is set to "Bundle", the bundle of content to install
129+
// is specified directly. In this case, no interaction with ClusterCatalog
130+
// resources is necessary. When using the Bundle sourceType, the bundle
131+
// field must also be set.
132+
// </opcon:experimental:description>
121133
//
122134
// When this field is set to "Catalog", information for determining the
123135
// appropriate bundle of content to install will be fetched from
124136
// ClusterCatalog resources existing on the cluster.
125137
// When using the Catalog sourceType, the catalog field must also be set.
126138
//
127139
// +unionDiscriminator
128-
// +kubebuilder:validation:Enum:="Catalog"
140+
// <opcon:experimental:validation:Enum=Bundle;Catalog>
141+
// <opcon:standard:validation:Enum=Catalog>
129142
// +kubebuilder:validation:Required
130143
SourceType string `json:"sourceType"`
131144

145+
// bundle is used to configure how information is sourced from a bundle.
146+
// This field is required when sourceType is "Bundle", and forbidden otherwise.
147+
//
148+
// +optional.
149+
// <opcon:experimental>
150+
Bundle *BundleSource `json:"bundle,omitempty"`
151+
132152
// catalog is used to configure how information is sourced from a catalog.
133153
// This field is required when sourceType is "Catalog", and forbidden otherwise.
134154
//
@@ -180,6 +200,60 @@ type ClusterExtensionConfig struct {
180200
Inline *apiextensionsv1.JSON `json:"inline,omitempty"`
181201
}
182202

203+
// BundleSource defines the configuration used to retrieve a bundle directly from
204+
// its OCI-based image reference.
205+
type BundleSource struct {
206+
// ref allows users to define the reference to a container image containing bundle contents.
207+
// ref is required.
208+
// ref can not be more than 1000 characters.
209+
//
210+
// A reference can be broken down into 3 parts - the domain, name, and identifier.
211+
//
212+
// The domain is typically the registry where an image is located.
213+
// It must be alphanumeric characters (lowercase and uppercase) separated by the "." character.
214+
// Hyphenation is allowed, but the domain must start and end with alphanumeric characters.
215+
// Specifying a port to use is also allowed by adding the ":" character followed by numeric values.
216+
// The port must be the last value in the domain.
217+
// Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080".
218+
//
219+
// The name is typically the repository in the registry where an image is located.
220+
// It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters.
221+
// Multiple names can be concatenated with the "/" character.
222+
// The domain and name are combined using the "/" character.
223+
// Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod".
224+
// An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle".
225+
//
226+
// The identifier is typically the tag or digest for an image reference and is present at the end of the reference.
227+
// It starts with a separator character used to distinguish the end of the name and beginning of the identifier.
228+
// For a digest-based reference, the "@" character is the separator.
229+
// For a tag-based reference, the ":" character is the separator.
230+
// An identifier is required in the reference.
231+
//
232+
// Digest-based references must contain an algorithm reference immediately after the "@" separator.
233+
// The algorithm reference must be followed by the ":" character and an encoded string.
234+
// The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters.
235+
// Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58".
236+
// The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters.
237+
//
238+
// Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters.
239+
// The tag must not be longer than 127 characters.
240+
//
241+
// An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"
242+
// An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest"
243+
//
244+
// +kubebuilder:validation:Required
245+
// +kubebuilder:validation:MaxLength:=1000
246+
// +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."
247+
// +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."
248+
// +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" || self.find(':.*$') != \"\"",message="must end with a digest or a tag"
249+
// +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"
250+
// +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"
251+
// +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."
252+
// +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"
253+
// +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)"
254+
Ref string `json:"ref"`
255+
}
256+
183257
// CatalogFilter defines the attributes used to identify and filter content from a catalog.
184258
type CatalogFilter struct {
185259
// packageName is a reference to the name of the package to be installed

api/v1/zz_generated.deepcopy.go

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/operator-controller/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ func run() error {
404404
return httputil.BuildHTTPClient(cpwCatalogd)
405405
})
406406

407-
resolver := &resolve.CatalogResolver{
407+
resolver := &resolve.MultiResolver{}
408+
resolver.RegisterType(ocv1.SourceTypeCatalog, &resolve.CatalogResolver{
408409
WalkCatalogsFunc: resolve.CatalogWalker(
409410
func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) {
410411
var catalogs ocv1.ClusterCatalogList
@@ -418,6 +419,12 @@ func run() error {
418419
Validations: []resolve.ValidationFunc{
419420
resolve.NoDependencyValidation,
420421
},
422+
})
423+
if features.OperatorControllerFeatureGate.Enabled(features.DirectBundleInstall) {
424+
resolver.RegisterType(ocv1.SourceTypeBundle, &resolve.BundleResolver{
425+
ImagePuller: imagePuller,
426+
ImageCache: imageCache,
427+
})
421428
}
422429

423430
aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig())

docs/api-reference/olmv1-api-reference.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ _Appears in:_
5050
| `version` _string_ | version is a required field and is a reference to the version that this bundle represents<br />version follows the semantic versioning standard as defined in https://semver.org/. | | Required: \{\} <br /> |
5151

5252

53+
#### BundleSource
54+
55+
56+
57+
BundleSource defines the configuration used to retrieve a bundle directly from
58+
its OCI-based image reference.
59+
60+
61+
62+
_Appears in:_
63+
- [SourceConfig](#sourceconfig)
64+
65+
| Field | Description | Default | Validation |
66+
| --- | --- | --- | --- |
67+
| `ref` _string_ | ref allows users to define the reference to a container image containing bundle contents.<br />ref is required.<br />ref can not be more than 1000 characters.<br /><br />A reference can be broken down into 3 parts - the domain, name, and identifier.<br /><br />The domain is typically the registry where an image is located.<br />It must be alphanumeric characters (lowercase and uppercase) separated by the "." character.<br />Hyphenation is allowed, but the domain must start and end with alphanumeric characters.<br />Specifying a port to use is also allowed by adding the ":" character followed by numeric values.<br />The port must be the last value in the domain.<br />Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080".<br /><br />The name is typically the repository in the registry where an image is located.<br />It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters.<br />Multiple names can be concatenated with the "/" character.<br />The domain and name are combined using the "/" character.<br />Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod".<br />An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle".<br /><br />The identifier is typically the tag or digest for an image reference and is present at the end of the reference.<br />It starts with a separator character used to distinguish the end of the name and beginning of the identifier.<br />For a digest-based reference, the "@" character is the separator.<br />For a tag-based reference, the ":" character is the separator.<br />An identifier is required in the reference.<br /><br />Digest-based references must contain an algorithm reference immediately after the "@" separator.<br />The algorithm reference must be followed by the ":" character and an encoded string.<br />The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters.<br />Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58".<br />The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters.<br /><br />Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters.<br />The tag must not be longer than 127 characters.<br /><br />An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"<br />An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest" | | MaxLength: 1000 <br />Required: \{\} <br /> |
68+
69+
5370
#### CRDUpgradeSafetyEnforcement
5471

5572
_Underlying type:_ _string_
@@ -459,13 +476,17 @@ _Appears in:_
459476
SourceConfig is a discriminated union which selects the installation source.
460477

461478

479+
<opcon:experimental:validation:XValidation:rule="has(self.sourceType) && self.sourceType == 'Bundle' ? has(self.bundle) : !has(self.bundle)",message="bundle is required when sourceType is Bundle, and forbidden otherwise">
480+
481+
462482

463483
_Appears in:_
464484
- [ClusterExtensionSpec](#clusterextensionspec)
465485

466486
| Field | Description | Default | Validation |
467487
| --- | --- | --- | --- |
468-
| `sourceType` _string_ | sourceType is a required reference to the type of install source.<br /><br />Allowed values are "Catalog"<br /><br />When this field is set to "Catalog", information for determining the<br />appropriate bundle of content to install will be fetched from<br />ClusterCatalog resources existing on the cluster.<br />When using the Catalog sourceType, the catalog field must also be set. | | Enum: [Catalog] <br />Required: \{\} <br /> |
488+
| `sourceType` _string_ | sourceType is a required reference to the type of install source.<br /><br />Allowed values are <opcon:experimental:description>"Bundle" or </opcon:experimental:description>"Catalog"<br /><br /><opcon:experimental:description><br />When this field is set to "Bundle", the bundle of content to install<br />is specified directly. In this case, no interaction with ClusterCatalog<br />resources is necessary. When using the Bundle sourceType, the bundle<br />field must also be set.<br /></opcon:experimental:description><br /><br />When this field is set to "Catalog", information for determining the<br />appropriate bundle of content to install will be fetched from<br />ClusterCatalog resources existing on the cluster.<br />When using the Catalog sourceType, the catalog field must also be set.<br /><br /><opcon:experimental:validation:Enum=Bundle;Catalog><br /><opcon:standard:validation:Enum=Catalog> | | Required: \{\} <br /> |
489+
| `bundle` _[BundleSource](#bundlesource)_ | bundle is used to configure how information is sourced from a bundle.<br />This field is required when sourceType is "Bundle", and forbidden otherwise.<br /><br /><opcon:experimental> | | |
469490
| `catalog` _[CatalogFilter](#catalogfilter)_ | catalog is used to configure how information is sourced from a catalog.<br />This field is required when sourceType is "Catalog", and forbidden otherwise. | | |
470491

471492

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ require (
126126
github.com/gobuffalo/flect v1.0.3 // indirect
127127
github.com/gobwas/glob v0.2.3 // indirect
128128
github.com/gogo/protobuf v1.3.2 // indirect
129+
github.com/golang-migrate/migrate/v4 v4.19.0 // indirect
129130
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
130131
github.com/golang/protobuf v1.5.4 // indirect
131132
github.com/google/btree v1.1.3 // indirect

hack/tools/crd-generator/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -258,13 +258,13 @@ func formatDescription(description string, channel string, name string) string {
258258
startTag := "<opcon:experimental:description>"
259259
endTag := "</opcon:experimental:description>"
260260
if channel == StandardChannel && strings.Contains(description, startTag) {
261-
regexPattern := `\n*` + regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag) + `\n*`
261+
regexPattern := regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag)
262262
re := regexp.MustCompile(regexPattern)
263263
match := re.FindStringSubmatch(description)
264264
if len(match) != 2 {
265265
log.Fatalf("Invalid <opcon:experimental:description> tag for %s", name)
266266
}
267-
description = re.ReplaceAllString(description, "\n\n")
267+
description = re.ReplaceAllString(description, "")
268268
} else {
269269
description = strings.ReplaceAll(description, startTag, "")
270270
description = strings.ReplaceAll(description, endTag, "")

helm/experimental.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ operatorControllerFeatures:
1111
- PreflightPermissions
1212
- HelmChartSupport
1313
- BoxcutterRuntime
14+
- DirectBundleInstall
1415

1516
# List of enabled experimental features for catalogd
1617
# Use with {{- if has "FeatureGate" .Values.catalogdFeatures }}

0 commit comments

Comments
 (0)