Skip to content

Commit 3b9eb66

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 3b9eb66

File tree

12 files changed

+494
-9
lines changed

12 files changed

+494
-9
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())

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 }}

helm/olmv1/base/operator-controller/crd/experimental/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,93 @@ spec:
225225
catalog:
226226
packageName: example-package
227227
properties:
228+
bundle:
229+
description: |-
230+
bundle is used to configure how information is sourced from a bundle.
231+
This field is required when sourceType is "Bundle", and forbidden otherwise.
232+
properties:
233+
ref:
234+
description: |-
235+
ref allows users to define the reference to a container image containing bundle contents.
236+
ref is required.
237+
ref can not be more than 1000 characters.
238+
239+
A reference can be broken down into 3 parts - the domain, name, and identifier.
240+
241+
The domain is typically the registry where an image is located.
242+
It must be alphanumeric characters (lowercase and uppercase) separated by the "." character.
243+
Hyphenation is allowed, but the domain must start and end with alphanumeric characters.
244+
Specifying a port to use is also allowed by adding the ":" character followed by numeric values.
245+
The port must be the last value in the domain.
246+
Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080".
247+
248+
The name is typically the repository in the registry where an image is located.
249+
It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters.
250+
Multiple names can be concatenated with the "/" character.
251+
The domain and name are combined using the "/" character.
252+
Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod".
253+
An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle".
254+
255+
The identifier is typically the tag or digest for an image reference and is present at the end of the reference.
256+
It starts with a separator character used to distinguish the end of the name and beginning of the identifier.
257+
For a digest-based reference, the "@" character is the separator.
258+
For a tag-based reference, the ":" character is the separator.
259+
An identifier is required in the reference.
260+
261+
Digest-based references must contain an algorithm reference immediately after the "@" separator.
262+
The algorithm reference must be followed by the ":" character and an encoded string.
263+
The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters.
264+
Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58".
265+
The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters.
266+
267+
Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters.
268+
The tag must not be longer than 127 characters.
269+
270+
An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"
271+
An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest"
272+
maxLength: 1000
273+
type: string
274+
x-kubernetes-validations:
275+
- message: must start with a valid domain. valid domains must
276+
be alphanumeric characters (lowercase and uppercase) separated
277+
by the "." character.
278+
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')
279+
- message: a valid name is required. valid names must contain
280+
lowercase alphanumeric characters separated only by the
281+
".", "_", "__", "-" characters.
282+
rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)')
283+
!= ""
284+
- message: must end with a digest or a tag
285+
rule: self.find('(@.*:)') != "" || self.find(':.*$') !=
286+
""
287+
- message: tag is invalid. the tag must not be more than 127
288+
characters
289+
rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'')
290+
!= "" ? self.find('':.*$'').substring(1).size() <= 127
291+
: true) : true'
292+
- message: tag is invalid. valid tags must begin with a word
293+
character (alphanumeric + "_") followed by word characters
294+
or ".", and "-" characters
295+
rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'')
296+
!= "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'')
297+
: true) : true'
298+
- message: digest algorithm is not valid. valid algorithms
299+
must start with an uppercase or lowercase alpha character
300+
followed by alphanumeric characters and may contain the
301+
"-", "_", "+", and "." characters.
302+
rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'')
303+
: true'
304+
- message: digest is not valid. the encoded string must be
305+
at least 32 characters
306+
rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size()
307+
>= 32 : true'
308+
- message: digest is not valid. the encoded string must only
309+
contain hex characters (A-F, a-f, 0-9)
310+
rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'')
311+
: true'
312+
required:
313+
- ref
314+
type: object
228315
catalog:
229316
description: |-
230317
catalog is used to configure how information is sourced from a catalog.
@@ -474,13 +561,19 @@ spec:
474561
description: |-
475562
sourceType is a required reference to the type of install source.
476563
477-
Allowed values are "Catalog"
564+
Allowed values are "Bundle" or "Catalog"
565+
566+
When this field is set to "Bundle", the bundle of content to install
567+
is specified directly. In this case, no interaction with ClusterCatalog
568+
resources is necessary. When using the Bundle sourceType, the bundle
569+
field must also be set.
478570
479571
When this field is set to "Catalog", information for determining the
480572
appropriate bundle of content to install will be fetched from
481573
ClusterCatalog resources existing on the cluster.
482574
When using the Catalog sourceType, the catalog field must also be set.
483575
enum:
576+
- Bundle
484577
- Catalog
485578
type: string
486579
required:

0 commit comments

Comments
 (0)