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