diff --git a/api/v1alpha1/clusterextension_types.go b/api/v1alpha1/clusterextension_types.go
index b47edd3ed..3f92d8b3c 100644
--- a/api/v1alpha1/clusterextension_types.go
+++ b/api/v1alpha1/clusterextension_types.go
@@ -74,22 +74,31 @@ type ClusterExtensionSpec struct {
Install ClusterExtensionInstallConfig `json:"install"`
}
-const SourceTypeCatalog = "Catalog"
+const (
+ SourceTypeCatalog = "Catalog"
+ SourceTypeBundle = "Bundle"
+)
// SourceConfig is a discriminated union which selects the installation source.
// +union
-// +kubebuilder:validation:XValidation:rule="self.sourceType == 'Catalog' && has(self.catalog)",message="sourceType Catalog requires catalog field"
+
+// +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"
+// +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 ["Catalog", "Bundle"]
//
// 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.
//
- // +unionDiscriminator
- // +kubebuilder:validation:Enum:="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.
+ //
+ // +unionDiscriminatorq
+ // +kubebuilder:validation:Enum:=Bundle;Catalog
SourceType string `json:"sourceType"`
// catalog is used to configure how information is sourced from a catalog. This field must be defined when sourceType is set to "Catalog",
@@ -97,6 +106,12 @@ type SourceConfig struct {
//
// +optional.
Catalog *CatalogSource `json:"catalog,omitempty"`
+
+ // bundle is used to configure how information is sourced from a bundle. This field must be defined when sourceType is set to "Bundle",
+ // and must be the only field defined for this sourceType.
+ //
+ // +optional.
+ Bundle *BundleSource `json:"bundle,omitempty"`
}
// ClusterExtensionInstallConfig is a union which selects the clusterExtension installation config.
@@ -443,6 +458,14 @@ type CatalogSource struct {
UpgradeConstraintPolicy UpgradeConstraintPolicy `json:"upgradeConstraintPolicy,omitempty"`
}
+type BundleSource struct {
+ // ref is an OCI reference to an extension bundle image. Images referenced
+ // must conform to the registry+v1 bundle format.
+
+ //+kubebuilder:validation:Required
+ Ref string `json:"ref"`
+}
+
// ServiceAccountReference references a serviceAccount.
type ServiceAccountReference struct {
// name is a required, immutable reference to the name of the ServiceAccount
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 23dcfeed3..ce9b43a06 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -41,6 +41,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
@@ -283,6 +298,11 @@ func (in *SourceConfig) DeepCopyInto(out *SourceConfig) {
*out = new(CatalogSource)
(*in).DeepCopyInto(*out)
}
+ if in.Bundle != nil {
+ in, out := &in.Bundle, &out.Bundle
+ *out = new(BundleSource)
+ **out = **in
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceConfig.
diff --git a/cmd/manager/main.go b/cmd/manager/main.go
index 3d83c0a32..e137b33e0 100644
--- a/cmd/manager/main.go
+++ b/cmd/manager/main.go
@@ -258,7 +258,11 @@ func main() {
return httputil.BuildHTTPClient(certPoolWatcher)
})
- resolver := &resolve.CatalogResolver{
+ bundleResolver := &resolve.BundleResolver{
+ Unpacker: unpacker,
+ BrittleUnpackerCacheDir: unpacker.BaseCachePath,
+ }
+ catalogResolver := &resolve.CatalogResolver{
WalkCatalogsFunc: resolve.CatalogWalker(
func(ctx context.Context, option ...client.ListOption) ([]catalogd.ClusterCatalog, error) {
var catalogs catalogd.ClusterCatalogList
@@ -273,6 +277,9 @@ func main() {
resolve.NoDependencyValidation,
},
}
+ resolver := resolve.MultiResolver{}
+ resolver.RegisterType(ocv1alpha1.SourceTypeBundle, bundleResolver)
+ resolver.RegisterType(ocv1alpha1.SourceTypeCatalog, catalogResolver)
aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig())
if err != nil {
diff --git a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml
index 26f3f5266..d2dfc0943 100644
--- a/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml
+++ b/config/base/crd/bases/olm.operatorframework.io_clusterextensions.yaml
@@ -324,6 +324,16 @@ spec:
catalog:
packageName: example-package
properties:
+ bundle:
+ description: |-
+ bundle is used to configure how information is sourced from a bundle. This field must be defined when sourceType is set to "Bundle",
+ and must be the only field defined for this sourceType.
+ properties:
+ ref:
+ type: string
+ required:
+ - ref
+ type: object
catalog:
description: |-
catalog is used to configure how information is sourced from a catalog. This field must be defined when sourceType is set to "Catalog",
@@ -568,20 +578,31 @@ spec:
description: |-
sourceType is a required reference to the type of install source.
- Allowed values are ["Catalog"]
+ Allowed values are ["Catalog", "Bundle"]
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.
+
+ 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.
enum:
+ - Bundle
- Catalog
type: string
required:
- sourceType
type: object
x-kubernetes-validations:
- - message: sourceType Catalog requires catalog field
- rule: self.sourceType == 'Catalog' && has(self.catalog)
+ - message: bundle is required when sourceType is Bundle, and forbidden
+ otherwise
+ rule: 'has(self.sourceType) && self.sourceType == ''Bundle'' ?has(self.bundle)
+ : !has(self.bundle)'
+ - message: catalog is required when sourceType is Catalog, and forbidden
+ otherwise
+ rule: 'has(self.sourceType) && self.sourceType == ''Catalog'' ?has(self.catalog)
+ : !has(self.catalog)'
required:
- install
- source
diff --git a/config/samples/cloudnative-pg-clusterextension.yaml b/config/samples/cloudnative-pg-clusterextension.yaml
index 9a3e6bb34..b71593ae0 100644
--- a/config/samples/cloudnative-pg-clusterextension.yaml
+++ b/config/samples/cloudnative-pg-clusterextension.yaml
@@ -29,10 +29,9 @@ metadata:
name: cloudnative-pg
spec:
source:
- sourceType: Catalog
- catalog:
- packageName: cloudnative-pg
- version: "1.24.1"
+ sourceType: Bundle
+ bundle:
+ ref: quay.io/operatorhubio/cloudnative-pg@sha256:e960f799f3d2b2dd5ecc74bc576476fe9c70de6486ba5ffc7d6ef333bba186bc
install:
namespace: cloudnative-pg
serviceAccount:
diff --git a/config/samples/olm_v1alpha1_clusterextension.yaml b/config/samples/olm_v1alpha1_clusterextension.yaml
index 7536c3d90..626deb5b8 100644
--- a/config/samples/olm_v1alpha1_clusterextension.yaml
+++ b/config/samples/olm_v1alpha1_clusterextension.yaml
@@ -273,11 +273,12 @@ metadata:
name: argocd
spec:
source:
- sourceType: Catalog
- catalog:
- packageName: argocd-operator
- version: 0.6.0
+ sourceType: Bundle
+ bundle:
+ ref: quay.io/operatorhubio/argocd-operator@sha256:d538c45a813b38ef0e44f40d279dc2653f97ca901fb660da5d7fe499d51ad3b3
install:
namespace: argocd
serviceAccount:
name: argocd-installer
+ values:
+ watchNamespace: argocd
diff --git a/docs/api-reference/operator-controller-api-reference.md b/docs/api-reference/operator-controller-api-reference.md
index f56a8eb31..124a3d2b5 100644
--- a/docs/api-reference/operator-controller-api-reference.md
+++ b/docs/api-reference/operator-controller-api-reference.md
@@ -31,6 +31,22 @@ _Appears in:_
| `version` _string_ | version is a required field and is a reference
to the version that this bundle represents | | |
+#### BundleSource
+
+
+
+
+
+
+
+_Appears in:_
+- [SourceConfig](#sourceconfig)
+
+| Field | Description | Default | Validation |
+| --- | --- | --- | --- |
+| `ref` _string_ | | | Required: \{\}
|
+
+
#### CRDUpgradeSafetyPolicy
_Underlying type:_ _string_
@@ -248,7 +264,7 @@ _Appears in:_
-SourceConfig is a discriminated union which selects the installation source.
+
@@ -257,8 +273,9 @@ _Appears in:_
| 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]
|
+| `sourceType` _string_ | sourceType is a required reference to the type of install source.
Allowed values are ["Catalog", "Bundle"]
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.
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. | | Enum: [Bundle Catalog]
|
| `catalog` _[CatalogSource](#catalogsource)_ | catalog is used to configure how information is sourced from a catalog. This field must be defined when sourceType is set to "Catalog",
and must be the only field defined for this sourceType. | | |
+| `bundle` _[BundleSource](#bundlesource)_ | bundle is used to configure how information is sourced from a bundle. This field must be defined when sourceType is set to "Bundle",
and must be the only field defined for this sourceType. | | |
#### UpgradeConstraintPolicy
diff --git a/go.mod b/go.mod
index 9d9a7b32a..be7243ab2 100644
--- a/go.mod
+++ b/go.mod
@@ -113,6 +113,7 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-migrate/migrate/v4 v4.18.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.2 // indirect
diff --git a/go.sum b/go.sum
index 479b2dcac..2328c9ba3 100644
--- a/go.sum
+++ b/go.sum
@@ -751,6 +751,8 @@ go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR
go.starlark.net v0.0.0-20230612165344-9532f5667272 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8=
go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
+go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
diff --git a/internal/resolve/bundle.go b/internal/resolve/bundle.go
new file mode 100644
index 000000000..1519583dd
--- /dev/null
+++ b/internal/resolve/bundle.go
@@ -0,0 +1,72 @@
+package resolve
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "path/filepath"
+
+ bsemver "github.com/blang/semver/v4"
+ "github.com/containers/image/v5/docker/reference"
+
+ "github.com/operator-framework/operator-registry/alpha/action"
+ "github.com/operator-framework/operator-registry/alpha/declcfg"
+
+ ocv1alpha1 "github.com/operator-framework/operator-controller/api/v1alpha1"
+ "github.com/operator-framework/operator-controller/internal/bundleutil"
+ "github.com/operator-framework/operator-controller/internal/rukpak/source"
+)
+
+type BundleResolver struct {
+ Unpacker source.Unpacker
+ BrittleUnpackerCacheDir string
+}
+
+func (r *BundleResolver) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterExtension, _ *ocv1alpha1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) {
+ res, err := r.Unpacker.Unpack(ctx, &source.BundleSource{
+ Name: ext.Name,
+ Type: source.SourceTypeImage,
+ Image: &source.ImageSource{
+ Ref: ext.Spec.Source.Bundle.Ref,
+ },
+ })
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ if res.State != source.StateUnpacked {
+ return nil, nil, nil, fmt.Errorf("bundle not unpacked: %v", res.Message)
+ }
+
+ ref, err := reference.ParseNamed(res.ResolvedSource.Image.Ref)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ canonicalRef, ok := ref.(reference.Canonical)
+ if !ok {
+ return nil, nil, nil, errors.New("expected canonical reference")
+ }
+ bundlePath := filepath.Join(r.BrittleUnpackerCacheDir, ext.Name, canonicalRef.Digest().String())
+
+ // 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
+ // filesystem. This will be removed once the library is updated.
+
+ 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
+}
diff --git a/internal/resolve/resolver.go b/internal/resolve/resolver.go
index de9b952b0..8606a3c52 100644
--- a/internal/resolve/resolver.go
+++ b/internal/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 *ocv1alpha1.ClusterExtension, installedB
func (f Func) Resolve(ctx context.Context, ext *ocv1alpha1.ClusterExtension, installedBundle *ocv1alpha1.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 *ocv1alpha1.ClusterExtension, installedBundle *ocv1alpha1.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)
+}