Skip to content

Commit d6f2edb

Browse files
committed
poc: direct bundle install API + implementation
Signed-off-by: Joe Lanford <[email protected]>
1 parent 5f255ee commit d6f2edb

File tree

11 files changed

+319
-16
lines changed

11 files changed

+319
-16
lines changed

api/v1/clusterextension_types.go

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,27 +104,41 @@ type ClusterExtensionSpec struct {
104104
Install *ClusterExtensionInstallConfig `json:"install,omitempty"`
105105
}
106106

107-
const SourceTypeCatalog = "Catalog"
107+
const (
108+
SourceTypeBundle = "Bundle"
109+
SourceTypeCatalog = "Catalog"
110+
)
108111

109112
// SourceConfig is a discriminated union which selects the installation source.
110113
//
111114
// +union
115+
// +kubebuilder: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"
112116
// +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"
113117
type SourceConfig struct {
114118
// sourceType is a required reference to the type of install source.
115119
//
116-
// Allowed values are "Catalog"
120+
// Allowed values are "Bundle" or "Catalog"
121+
//
122+
// When this field is set to "Bundle", the bundle of content to install is specified
123+
// directly. In this case, no interaction with ClusterCatalog resources is necessary.
124+
// When using the Bundle sourceType, the bundle field must also be set.
117125
//
118126
// When this field is set to "Catalog", information for determining the
119127
// appropriate bundle of content to install will be fetched from
120128
// ClusterCatalog resources existing on the cluster.
121129
// When using the Catalog sourceType, the catalog field must also be set.
122130
//
123131
// +unionDiscriminator
124-
// +kubebuilder:validation:Enum:="Catalog"
132+
// +kubebuilder:validation:Enum:=Bundle;Catalog
125133
// +kubebuilder:validation:Required
126134
SourceType string `json:"sourceType"`
127135

136+
// bundle is used to configure how information is sourced from a bundle.
137+
// This field is required when sourceType is "Bundle", and forbidden otherwise.
138+
//
139+
// +optional.
140+
Bundle *BundleSource `json:"bundle,omitempty"`
141+
128142
// catalog is used to configure how information is sourced from a catalog.
129143
// This field is required when sourceType is "Catalog", and forbidden otherwise.
130144
//
@@ -444,7 +458,61 @@ type CatalogSource struct {
444458
UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"`
445459
}
446460

447-
// ServiceAccountReference identifies the serviceAccount used fo install a ClusterExtension.
461+
// BundleSource defines the configuration used to retrieve a bundle directly from
462+
// its OCI-based image reference.
463+
type BundleSource struct {
464+
// ref allows users to define the reference to a container image containing bundle contents.
465+
// ref is required.
466+
// ref can not be more than 1000 characters.
467+
//
468+
// A reference can be broken down into 3 parts - the domain, name, and identifier.
469+
//
470+
// The domain is typically the registry where an image is located.
471+
// It must be alphanumeric characters (lowercase and uppercase) separated by the "." character.
472+
// Hyphenation is allowed, but the domain must start and end with alphanumeric characters.
473+
// Specifying a port to use is also allowed by adding the ":" character followed by numeric values.
474+
// The port must be the last value in the domain.
475+
// Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080".
476+
//
477+
// The name is typically the repository in the registry where an image is located.
478+
// It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters.
479+
// Multiple names can be concatenated with the "/" character.
480+
// The domain and name are combined using the "/" character.
481+
// Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod".
482+
// An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle".
483+
//
484+
// The identifier is typically the tag or digest for an image reference and is present at the end of the reference.
485+
// It starts with a separator character used to distinguish the end of the name and beginning of the identifier.
486+
// For a digest-based reference, the "@" character is the separator.
487+
// For a tag-based reference, the ":" character is the separator.
488+
// An identifier is required in the reference.
489+
//
490+
// Digest-based references must contain an algorithm reference immediately after the "@" separator.
491+
// The algorithm reference must be followed by the ":" character and an encoded string.
492+
// The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters.
493+
// Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58".
494+
// The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters.
495+
//
496+
// Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters.
497+
// The tag must not be longer than 127 characters.
498+
//
499+
// An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"
500+
// An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest"
501+
//
502+
// +kubebuilder:validation:Required
503+
// +kubebuilder:validation:MaxLength:=1000
504+
// +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."
505+
// +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."
506+
// +kubebuilder:validation:XValidation:rule="self.find('(@.*:)') != \"\" || self.find(':.*$') != \"\"",message="must end with a digest or a tag"
507+
// +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"
508+
// +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"
509+
// +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."
510+
// +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"
511+
// +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)"
512+
Ref string `json:"ref"`
513+
}
514+
515+
// ServiceAccountReference identifies the serviceAccount used to install a ClusterExtension.
448516
type ServiceAccountReference struct {
449517
// name is a required, immutable reference to the name of the ServiceAccount
450518
// to be used for installation and management of the content for the package

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/manager/main.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,11 @@ func main() {
258258
return httputil.BuildHTTPClient(certPoolWatcher)
259259
})
260260

261-
resolver := &resolve.CatalogResolver{
261+
bundleResolver := &resolve.BundleResolver{
262+
Unpacker: unpacker,
263+
BrittleUnpackerCacheDir: unpacker.BaseCachePath,
264+
}
265+
catalogResolver := &resolve.CatalogResolver{
262266
WalkCatalogsFunc: resolve.CatalogWalker(
263267
func(ctx context.Context, option ...client.ListOption) ([]catalogd.ClusterCatalog, error) {
264268
var catalogs catalogd.ClusterCatalogList
@@ -273,6 +277,9 @@ func main() {
273277
resolve.NoDependencyValidation,
274278
},
275279
}
280+
resolver := resolve.MultiResolver{}
281+
resolver.RegisterType(ocv1.SourceTypeBundle, bundleResolver)
282+
resolver.RegisterType(ocv1.SourceTypeCatalog, catalogResolver)
276283

277284
aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig())
278285
if err != nil {

config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,93 @@ spec:
190190
catalog:
191191
packageName: example-package
192192
properties:
193+
bundle:
194+
description: |-
195+
bundle is used to configure how information is sourced from a bundle.
196+
This field is required when sourceType is "Bundle", and forbidden otherwise.
197+
properties:
198+
ref:
199+
description: |-
200+
ref allows users to define the reference to a container image containing bundle contents.
201+
ref is required.
202+
ref can not be more than 1000 characters.
203+
204+
A reference can be broken down into 3 parts - the domain, name, and identifier.
205+
206+
The domain is typically the registry where an image is located.
207+
It must be alphanumeric characters (lowercase and uppercase) separated by the "." character.
208+
Hyphenation is allowed, but the domain must start and end with alphanumeric characters.
209+
Specifying a port to use is also allowed by adding the ":" character followed by numeric values.
210+
The port must be the last value in the domain.
211+
Some examples of valid domain values are "registry.mydomain.io", "quay.io", "my-registry.io:8080".
212+
213+
The name is typically the repository in the registry where an image is located.
214+
It must contain lowercase alphanumeric characters separated only by the ".", "_", "__", "-" characters.
215+
Multiple names can be concatenated with the "/" character.
216+
The domain and name are combined using the "/" character.
217+
Some examples of valid name values are "operatorhubio/bundle", "bundle", "my-bundle.prod".
218+
An example of the domain and name parts of a reference being combined is "quay.io/operatorhubio/bundle".
219+
220+
The identifier is typically the tag or digest for an image reference and is present at the end of the reference.
221+
It starts with a separator character used to distinguish the end of the name and beginning of the identifier.
222+
For a digest-based reference, the "@" character is the separator.
223+
For a tag-based reference, the ":" character is the separator.
224+
An identifier is required in the reference.
225+
226+
Digest-based references must contain an algorithm reference immediately after the "@" separator.
227+
The algorithm reference must be followed by the ":" character and an encoded string.
228+
The algorithm must start with an uppercase or lowercase alpha character followed by alphanumeric characters and may contain the "-", "_", "+", and "." characters.
229+
Some examples of valid algorithm values are "sha256", "sha256+b64u", "multihash+base58".
230+
The encoded string following the algorithm must be hex digits (a-f, A-F, 0-9) and must be a minimum of 32 characters.
231+
232+
Tag-based references must begin with a word character (alphanumeric + "_") followed by word characters or ".", and "-" characters.
233+
The tag must not be longer than 127 characters.
234+
235+
An example of a valid digest-based image reference is "quay.io/operatorhubio/catalog@sha256:200d4ddb2a73594b91358fe6397424e975205bfbe44614f5846033cad64b3f05"
236+
An example of a valid tag-based image reference is "quay.io/operatorhubio/catalog:latest"
237+
maxLength: 1000
238+
type: string
239+
x-kubernetes-validations:
240+
- message: must start with a valid domain. valid domains must
241+
be alphanumeric characters (lowercase and uppercase) separated
242+
by the "." character.
243+
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')
244+
- message: a valid name is required. valid names must contain
245+
lowercase alphanumeric characters separated only by the
246+
".", "_", "__", "-" characters.
247+
rule: self.find('(\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?((\\/[a-z0-9]+((([._]|__|[-]*)[a-z0-9]+)+)?)+)?)')
248+
!= ""
249+
- message: must end with a digest or a tag
250+
rule: self.find('(@.*:)') != "" || self.find(':.*$') !=
251+
""
252+
- message: tag is invalid. the tag must not be more than 127
253+
characters
254+
rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'')
255+
!= "" ? self.find('':.*$'').substring(1).size() <= 127
256+
: true) : true'
257+
- message: tag is invalid. valid tags must begin with a word
258+
character (alphanumeric + "_") followed by word characters
259+
or ".", and "-" characters
260+
rule: 'self.find(''(@.*:)'') == "" ? (self.find('':.*$'')
261+
!= "" ? self.find('':.*$'').matches('':[\\w][\\w.-]*$'')
262+
: true) : true'
263+
- message: digest algorithm is not valid. valid algorithms
264+
must start with an uppercase or lowercase alpha character
265+
followed by alphanumeric characters and may contain the
266+
"-", "_", "+", and "." characters.
267+
rule: 'self.find(''(@.*:)'') != "" ? self.find(''(@.*:)'').matches(''(@[A-Za-z][A-Za-z0-9]*([-_+.][A-Za-z][A-Za-z0-9]*)*[:])'')
268+
: true'
269+
- message: digest is not valid. the encoded string must be
270+
at least 32 characters
271+
rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').substring(1).size()
272+
>= 32 : true'
273+
- message: digest is not valid. the encoded string must only
274+
contain hex characters (A-F, a-f, 0-9)
275+
rule: 'self.find(''(@.*:)'') != "" ? self.find('':.*$'').matches('':[0-9A-Fa-f]*$'')
276+
: true'
277+
required:
278+
- ref
279+
type: object
193280
catalog:
194281
description: |-
195282
catalog is used to configure how information is sourced from a catalog.
@@ -439,19 +526,28 @@ spec:
439526
description: |-
440527
sourceType is a required reference to the type of install source.
441528
442-
Allowed values are "Catalog"
529+
Allowed values are "Bundle" or "Catalog"
530+
531+
When this field is set to "Bundle", the bundle of content to install is specified
532+
directly. In this case, no interaction with ClusterCatalog resources is necessary.
533+
When using the Bundle sourceType, the bundle field must also be set.
443534
444535
When this field is set to "Catalog", information for determining the
445536
appropriate bundle of content to install will be fetched from
446537
ClusterCatalog resources existing on the cluster.
447538
When using the Catalog sourceType, the catalog field must also be set.
448539
enum:
540+
- Bundle
449541
- Catalog
450542
type: string
451543
required:
452544
- sourceType
453545
type: object
454546
x-kubernetes-validations:
547+
- message: bundle is required when sourceType is Bundle, and forbidden
548+
otherwise
549+
rule: 'has(self.sourceType) && self.sourceType == ''Bundle'' ?has(self.bundle)
550+
: !has(self.bundle)'
455551
- message: catalog is required when sourceType is Catalog, and forbidden
456552
otherwise
457553
rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ?

config/samples/cloudnative-pg-clusterextension.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@ metadata:
2929
name: cloudnative-pg
3030
spec:
3131
source:
32-
sourceType: Catalog
33-
catalog:
34-
packageName: cloudnative-pg
35-
version: "1.24.1"
32+
sourceType: Bundle
33+
bundle:
34+
ref: quay.io/operatorhubio/cloudnative-pg@sha256:e960f799f3d2b2dd5ecc74bc576476fe9c70de6486ba5ffc7d6ef333bba186bc
3635
install:
3736
namespace: cloudnative-pg
3837
serviceAccount:

config/samples/olm_v1_clusterextension.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,11 @@ spec:
279279
serviceAccount:
280280
name: argocd-installer
281281
source:
282-
sourceType: Catalog
283-
catalog:
284-
packageName: argocd-operator
285-
version: 0.6.0
282+
sourceType: Bundle
283+
bundle:
284+
ref: quay.io/operatorhubio/argocd-operator@sha256:d538c45a813b38ef0e44f40d279dc2653f97ca901fb660da5d7fe499d51ad3b3
285+
template:
286+
valuesSources:
287+
- type: Inline
288+
inline:
289+
watchNamespace: argocd

0 commit comments

Comments
 (0)