diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 0ebce0f716..8e777d9453 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -404,22 +404,31 @@ func run() error { return httputil.BuildHTTPClient(cpwCatalogd) }) - resolver := &resolve.CatalogResolver{ - WalkCatalogsFunc: resolve.CatalogWalker( - func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) { - var catalogs ocv1.ClusterCatalogList - if err := cl.List(ctx, &catalogs, option...); err != nil { - return nil, err - } - return catalogs.Items, nil + resolver := &resolve.MultiResolver{ + CatalogResolver: resolve.CatalogResolver{ + WalkCatalogsFunc: resolve.CatalogWalker( + func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) { + var catalogs ocv1.ClusterCatalogList + if err := cl.List(ctx, &catalogs, option...); err != nil { + return nil, err + } + return catalogs.Items, nil + }, + catalogClient.GetPackage, + ), + Validations: []resolve.ValidationFunc{ + resolve.NoDependencyValidation, }, - catalogClient.GetPackage, - ), - Validations: []resolve.ValidationFunc{ - resolve.NoDependencyValidation, }, } + if features.OperatorControllerFeatureGate.Enabled(features.DirectBundleInstall) { + resolver.BundleResolver = &resolve.BundleResolver{ + ImagePuller: imagePuller, + ImageCache: imageCache, + } + } + aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig()) if err != nil { setupLog.Error(err, "unable to create apiextensions client") diff --git a/go.mod b/go.mod index 7f9232fbca..349694cce4 100644 --- a/go.mod +++ b/go.mod @@ -126,6 +126,7 @@ require ( github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-migrate/migrate/v4 v4.19.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect diff --git a/internal/operator-controller/features/features.go b/internal/operator-controller/features/features.go index 1abdf0a18a..7a05d1055b 100644 --- a/internal/operator-controller/features/features.go +++ b/internal/operator-controller/features/features.go @@ -18,6 +18,7 @@ const ( WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA" HelmChartSupport featuregate.Feature = "HelmChartSupport" BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime" + DirectBundleInstall featuregate.Feature = "DirectBundleInstall" ) var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ @@ -80,6 +81,13 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature PreRelease: featuregate.Alpha, LockToDefault: false, }, + + // DirectBundleInstall allows for direct bundle installation via annotation + DirectBundleInstall: { + Default: false, + PreRelease: featuregate.Alpha, + LockToDefault: false, + }, } var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate() diff --git a/internal/operator-controller/resolve/bundle.go b/internal/operator-controller/resolve/bundle.go new file mode 100644 index 0000000000..3539a57791 --- /dev/null +++ b/internal/operator-controller/resolve/bundle.go @@ -0,0 +1,85 @@ +package resolve + +import ( + "context" + "errors" + "fmt" + "io/fs" + "reflect" + + bsemver "github.com/blang/semver/v4" + + "github.com/operator-framework/operator-registry/alpha/action" + "github.com/operator-framework/operator-registry/alpha/declcfg" + + ocv1 "github.com/operator-framework/operator-controller/api/v1" + "github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil" + "github.com/operator-framework/operator-controller/internal/shared/util/image" +) + +const ( + directBundleInstallImageAnnotation = "olm.operatorframework.io/bundle-image" +) + +type BundleResolver struct { + ImagePuller image.Puller + ImageCache image.Cache +} + +func (r *BundleResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + if ext.Annotations == nil || ext.Annotations[directBundleInstallImageAnnotation] == "" { + return nil, nil, nil, fmt.Errorf("ClusterExtension is missing required annotation %s", directBundleInstallImageAnnotation) + } + bundleFS, canonicalRef, _, err := r.ImagePuller.Pull(ctx, ext.Name, ext.Annotations[directBundleInstallImageAnnotation], r.ImageCache) + if err != nil { + return nil, nil, nil, err + } + + // 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 + // fs.FS. This will be removed once the library is updated. + bundlePath, err := getDirFSPath(bundleFS) + if err != nil { + panic(fmt.Errorf("expected to be able to recover bundle path from bundleFS: %v", err)) + } + + // Render the bundle + 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 +} + +// A function to recover the underlying path string from os.DirFS +func getDirFSPath(f fs.FS) (string, error) { + v := reflect.ValueOf(f) + + // Check if the underlying type is a string (its kind) + if v.Kind() != reflect.String { + return "", fmt.Errorf("underlying type is not a string, it is %s", v.Kind()) + } + + // The type itself (os.dirFS) is unexported, but its Kind is a string. + // We can convert the reflect.Value back to a regular string using .Interface() + // after converting it to a basic string type. + path, ok := v.Convert(reflect.TypeOf("")).Interface().(string) + if !ok { + return "", fmt.Errorf("could not convert reflected value to string") + } + + return path, nil +} diff --git a/internal/operator-controller/resolve/resolver.go b/internal/operator-controller/resolve/resolver.go index 625111d631..333a719b29 100644 --- a/internal/operator-controller/resolve/resolver.go +++ b/internal/operator-controller/resolve/resolver.go @@ -19,3 +19,17 @@ type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { return f(ctx, ext, installedBundle) } + +// MultiResolver uses the CatalogResolver by default. It will use the currently internal,feature gated, and annotation-powered BundleResolver +// if it is non-nil and the necessary annotation is present +type MultiResolver struct { + CatalogResolver CatalogResolver + BundleResolver *BundleResolver +} + +func (m MultiResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) { + if m.BundleResolver != nil && ext.Annotations != nil && ext.Annotations[directBundleInstallImageAnnotation] != "" { + return m.BundleResolver.Resolve(ctx, ext, installedBundle) + } + return m.CatalogResolver.Resolve(ctx, ext, installedBundle) +}