Skip to content

Commit 28f3498

Browse files
author
Per Goncalves da Silva
committed
Add internal direct bundle install method
Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com>
1 parent 6604f2a commit 28f3498

File tree

6 files changed

+172
-13
lines changed

6 files changed

+172
-13
lines changed

cmd/bob/main.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
7+
"sigs.k8s.io/yaml"
8+
)
9+
10+
// Define the struct you want to unmarshal into.
11+
type ServerConfig struct {
12+
URL string `json:"url"`
13+
Port int `json:"port"`
14+
}
15+
16+
func main() {
17+
// Example 1: Your data is YAML
18+
yamlData := []byte(`
19+
url: "https://api.example.com"
20+
port: 8080
21+
`)
22+
23+
// Example 2: Your data is JSON
24+
jsonData := []byte(`{"url": "https://api.example.com", "port": 443}`)
25+
26+
// --- Unmarshal YAML ---
27+
var configFromYaml ServerConfig
28+
err := yaml.Unmarshal(yamlData, &configFromYaml)
29+
if err != nil {
30+
log.Fatalf("Failed to unmarshal YAML: %v", err)
31+
}
32+
fmt.Printf("Successfully unmarshaled from YAML: %+v\n", configFromYaml)
33+
34+
// --- Unmarshal JSON ---
35+
var configFromJson ServerConfig
36+
err = yaml.Unmarshal(jsonData, &configFromJson)
37+
if err != nil {
38+
log.Fatalf("Failed to unmarshal JSON: %v", err)
39+
}
40+
fmt.Printf("Successfully unmarshaled from JSON: %+v\n", configFromJson)
41+
}

cmd/operator-controller/main.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -404,22 +404,31 @@ func run() error {
404404
return httputil.BuildHTTPClient(cpwCatalogd)
405405
})
406406

407-
resolver := &resolve.CatalogResolver{
408-
WalkCatalogsFunc: resolve.CatalogWalker(
409-
func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) {
410-
var catalogs ocv1.ClusterCatalogList
411-
if err := cl.List(ctx, &catalogs, option...); err != nil {
412-
return nil, err
413-
}
414-
return catalogs.Items, nil
407+
resolver := &resolve.MultiResolver{
408+
CatalogResolver: resolve.CatalogResolver{
409+
WalkCatalogsFunc: resolve.CatalogWalker(
410+
func(ctx context.Context, option ...client.ListOption) ([]ocv1.ClusterCatalog, error) {
411+
var catalogs ocv1.ClusterCatalogList
412+
if err := cl.List(ctx, &catalogs, option...); err != nil {
413+
return nil, err
414+
}
415+
return catalogs.Items, nil
416+
},
417+
catalogClient.GetPackage,
418+
),
419+
Validations: []resolve.ValidationFunc{
420+
resolve.NoDependencyValidation,
415421
},
416-
catalogClient.GetPackage,
417-
),
418-
Validations: []resolve.ValidationFunc{
419-
resolve.NoDependencyValidation,
420422
},
421423
}
422424

425+
if features.OperatorControllerFeatureGate.Enabled(features.DirectBundleInstall) {
426+
resolver.BundleResolver = &resolve.BundleResolver{
427+
ImagePuller: imagePuller,
428+
ImageCache: imageCache,
429+
}
430+
}
431+
423432
aeClient, err := apiextensionsv1client.NewForConfig(mgr.GetConfig())
424433
if err != nil {
425434
setupLog.Error(err, "unable to create apiextensions client")

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/blang/semver/v4 v4.0.0
99
github.com/cert-manager/cert-manager v1.18.2
1010
github.com/containerd/containerd v1.7.28
11+
github.com/distribution/reference v0.6.0
1112
github.com/fsnotify/fsnotify v1.9.0
1213
github.com/go-logr/logr v1.4.3
1314
github.com/golang-jwt/jwt/v5 v5.3.0
@@ -88,7 +89,6 @@ require (
8889
github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect
8990
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
9091
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
91-
github.com/distribution/reference v0.6.0 // indirect
9292
github.com/docker/cli v28.4.0+incompatible // indirect
9393
github.com/docker/distribution v2.8.3+incompatible // indirect
9494
github.com/docker/docker v28.3.3+incompatible // indirect
@@ -126,6 +126,7 @@ require (
126126
github.com/gobuffalo/flect v1.0.3 // indirect
127127
github.com/gobwas/glob v0.2.3 // indirect
128128
github.com/gogo/protobuf v1.3.2 // indirect
129+
github.com/golang-migrate/migrate/v4 v4.19.0 // indirect
129130
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
130131
github.com/golang/protobuf v1.5.4 // indirect
131132
github.com/google/btree v1.1.3 // indirect

internal/operator-controller/features/features.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const (
1818
WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA"
1919
HelmChartSupport featuregate.Feature = "HelmChartSupport"
2020
BoxcutterRuntime featuregate.Feature = "BoxcutterRuntime"
21+
DirectBundleInstall featuregate.Feature = "DirectBundleInstall"
2122
)
2223

2324
var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
@@ -80,6 +81,13 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
8081
PreRelease: featuregate.Alpha,
8182
LockToDefault: false,
8283
},
84+
85+
// DirectBundleInstall allows for direct bundle installation via annotation
86+
DirectBundleInstall: {
87+
Default: false,
88+
PreRelease: featuregate.Alpha,
89+
LockToDefault: false,
90+
},
8391
}
8492

8593
var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package resolve
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io/fs"
8+
"reflect"
9+
10+
bsemver "github.com/blang/semver/v4"
11+
12+
"github.com/operator-framework/operator-registry/alpha/action"
13+
"github.com/operator-framework/operator-registry/alpha/declcfg"
14+
15+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
16+
"github.com/operator-framework/operator-controller/internal/operator-controller/bundleutil"
17+
"github.com/operator-framework/operator-controller/internal/shared/util/image"
18+
)
19+
20+
const (
21+
directBundleInstallImageAnnotation = "olm.operatorframework.io/bundle-image"
22+
)
23+
24+
type BundleResolver struct {
25+
FallbackResolver Resolver
26+
ImagePuller image.Puller
27+
ImageCache image.Cache
28+
}
29+
30+
func (r *BundleResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, _ *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) {
31+
if ext.Annotations == nil || ext.Annotations[directBundleInstallImageAnnotation] == "" {
32+
return nil, nil, nil, fmt.Errorf("ClusterExtension is missing required annotation %s", directBundleInstallImageAnnotation)
33+
}
34+
bundleFS, canonicalRef, _, err := r.ImagePuller.Pull(ctx, ext.Name, ext.Annotations[directBundleInstallImageAnnotation], r.ImageCache)
35+
if err != nil {
36+
return nil, nil, nil, err
37+
}
38+
39+
// TODO: This is a temporary workaround to get the bundle from the filesystem
40+
// until the operator-registry library is updated to support reading from a
41+
// fs.FS. This will be removed once the library is updated.
42+
bundlePath, err := getDirFSPath(bundleFS)
43+
if err != nil {
44+
panic(fmt.Errorf("expected to be able to recover bundle path from bundleFS: %v", err))
45+
}
46+
47+
// Render the bundle
48+
render := action.Render{
49+
Refs: []string{bundlePath},
50+
AllowedRefMask: action.RefBundleDir,
51+
}
52+
fbc, err := render.Run(ctx)
53+
if err != nil {
54+
return nil, nil, nil, err
55+
}
56+
if len(fbc.Bundles) != 1 {
57+
return nil, nil, nil, errors.New("expected exactly one bundle")
58+
}
59+
bundle := fbc.Bundles[0]
60+
bundle.Image = canonicalRef.String()
61+
v, err := bundleutil.GetVersion(bundle)
62+
if err != nil {
63+
return nil, nil, nil, err
64+
}
65+
return &bundle, v, nil, nil
66+
}
67+
68+
// A function to recover the underlying path string from os.DirFS
69+
func getDirFSPath(f fs.FS) (string, error) {
70+
v := reflect.ValueOf(f)
71+
72+
// Check if the underlying type is a string (its kind)
73+
if v.Kind() != reflect.String {
74+
return "", fmt.Errorf("underlying type is not a string, it is %s", v.Kind())
75+
}
76+
77+
// The type itself (os.dirFS) is unexported, but its Kind is a string.
78+
// We can convert the reflect.Value back to a regular string using .Interface()
79+
// after converting it to a basic string type.
80+
path, ok := v.Convert(reflect.TypeOf("")).Interface().(string)
81+
if !ok {
82+
return "", fmt.Errorf("could not convert reflected value to string")
83+
}
84+
85+
return path, nil
86+
}

internal/operator-controller/resolve/resolver.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,17 @@ type Func func(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle
1919
func (f Func) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) {
2020
return f(ctx, ext, installedBundle)
2121
}
22+
23+
// MultiResolver uses the CatalogResolver by default. It will use the currently internal,feature gated, and annotation-powered BundleResolver
24+
// if it is non-nil and the necessary annotation is present
25+
type MultiResolver struct {
26+
CatalogResolver CatalogResolver
27+
BundleResolver *BundleResolver
28+
}
29+
30+
func (m MultiResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtension, installedBundle *ocv1.BundleMetadata) (*declcfg.Bundle, *bsemver.Version, *declcfg.Deprecation, error) {
31+
if m.BundleResolver != nil && ext.Annotations != nil && ext.Annotations[directBundleInstallImageAnnotation] != "" {
32+
return m.BundleResolver.Resolve(ctx, ext, installedBundle)
33+
}
34+
return m.CatalogResolver.Resolve(ctx, ext, installedBundle)
35+
}

0 commit comments

Comments
 (0)