Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions cmd/operator-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions internal/operator-controller/features/features.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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()
Expand Down
85 changes: 85 additions & 0 deletions internal/operator-controller/resolve/bundle.go
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to reflect in the annotation name that it is alpha feature, so something like:

alpha.olm.operatorframework.io/bundle-image-ref

)

type BundleResolver struct {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about to name it BundleImageRefResolver? IMHO, BundleResolver is too generic.

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)
}
Comment on lines +30 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is performed even in MultiResolver, before invoking BundleResolver. Hence, we can drop it here.

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)
Comment on lines +38 to +41
Copy link
Contributor

@tmshort tmshort Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't like the FS interface, as it's so limiting, and you're already needing to reflect it to get the proper path. I'd almost prefer to get rid of the FS interface, and simply use a path string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is really just me whining about the FS interface is all... not asking for a change unless you agree and really want to.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't disagree. But, atm, this FG is just an artifice to help us play around with the boxcutter status without having to deal with catalogs. Ultimately, I'd probably suggest moving away from FS and towards a formalized bundle interface that can surface all this kind of information without too many headaches

if err != nil {
panic(fmt.Errorf("expected to be able to recover bundle path from bundleFS: %v", err))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why panic instead of

return nil, nil, nil, 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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perhaps you can provide in the error message the number of bundles we got?

}
bundle := fbc.Bundles[0]
bundle.Image = canonicalRef.String()
v, err := bundleutil.GetVersion(bundle)
if err != nil {
return nil, nil, nil, err
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you provide the error context here?

}
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
}
14 changes: 14 additions & 0 deletions internal/operator-controller/resolve/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading