Skip to content

Commit 735a6d9

Browse files
committed
Add support for deploying OCI helm charts in OLM v1
* added support for deploying OCI helm charts which sits behind the HelmChartSupport feature gate * extend the Cache Store() method to allow storing of Helm charts * inspect chart archive contents * added MediaType to the LayerData struct Signed-off-by: Edmund Ochieng <[email protected]>
1 parent 1a27741 commit 735a6d9

File tree

7 files changed

+956
-6
lines changed

7 files changed

+956
-6
lines changed

internal/operator-controller/applier/helm.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@ import (
2626

2727
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2828
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
29+
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
2930
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
3031
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
3132
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
33+
imageutil "github.com/operator-framework/operator-controller/internal/shared/util/image"
3234
)
3335

3436
const (
@@ -209,6 +211,17 @@ func (h *Helm) buildHelmChart(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*char
209211
if err != nil {
210212
return nil, err
211213
}
214+
if features.OperatorControllerFeatureGate.Enabled(features.HelmChartSupport) {
215+
meta := new(chart.Metadata)
216+
if ok, _ := imageutil.IsBundleSourceChart(bundleFS, meta); ok {
217+
return imageutil.LoadChartFSWithOptions(
218+
bundleFS,
219+
fmt.Sprintf("%s-%s.tgz", meta.Name, meta.Version),
220+
imageutil.WithInstallNamespace(ext.Spec.Namespace),
221+
)
222+
}
223+
}
224+
212225
return h.BundleToHelmChartConverter.ToHelmChart(source.FromFS(bundleFS), ext.Spec.Namespace, watchNamespace)
213226
}
214227

internal/operator-controller/features/features.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const (
1616
SyntheticPermissions featuregate.Feature = "SyntheticPermissions"
1717
WebhookProviderCertManager featuregate.Feature = "WebhookProviderCertManager"
1818
WebhookProviderOpenshiftServiceCA featuregate.Feature = "WebhookProviderOpenshiftServiceCA"
19+
HelmChartSupport featuregate.Feature = "HelmChartSupport"
1920
)
2021

2122
var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
@@ -63,6 +64,14 @@ var operatorControllerFeatureGates = map[featuregate.Feature]featuregate.Feature
6364
PreRelease: featuregate.Alpha,
6465
LockToDefault: false,
6566
},
67+
68+
// HelmChartSupport enables support for installing,
69+
// updating and uninstalling Helm Charts via Cluster Extensions.
70+
HelmChartSupport: {
71+
Default: false,
72+
PreRelease: featuregate.Alpha,
73+
LockToDefault: false,
74+
},
6675
}
6776

6877
var OperatorControllerFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()

internal/shared/util/image/cache.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,19 @@ import (
1616
"github.com/containers/image/v5/docker/reference"
1717
"github.com/opencontainers/go-digest"
1818
ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
19+
"helm.sh/helm/v3/pkg/chart"
20+
"helm.sh/helm/v3/pkg/registry"
1921
"sigs.k8s.io/controller-runtime/pkg/log"
2022

2123
errorutil "github.com/operator-framework/operator-controller/internal/shared/util/error"
2224
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
2325
)
2426

2527
type LayerData struct {
26-
Reader io.Reader
27-
Index int
28-
Err error
28+
MediaType string
29+
Reader io.Reader
30+
Index int
31+
Err error
2932
}
3033

3134
type Cache interface {
@@ -128,8 +131,15 @@ func (a *diskCache) Store(ctx context.Context, ownerID string, srcRef reference.
128131
if layer.Err != nil {
129132
return fmt.Errorf("error reading layer[%d]: %w", layer.Index, layer.Err)
130133
}
131-
if _, err := archive.Apply(ctx, dest, layer.Reader, applyOpts...); err != nil {
132-
return fmt.Errorf("error applying layer[%d]: %w", layer.Index, err)
134+
switch layer.MediaType {
135+
case registry.ChartLayerMediaType:
136+
if err := storeChartLayer(dest, layer); err != nil {
137+
return err
138+
}
139+
default:
140+
if _, err := archive.Apply(ctx, dest, layer.Reader, applyOpts...); err != nil {
141+
return fmt.Errorf("error applying layer[%d]: %w", layer.Index, err)
142+
}
133143
}
134144
l.Info("applied layer", "layer", layer.Index)
135145
}
@@ -147,6 +157,29 @@ func (a *diskCache) Store(ctx context.Context, ownerID string, srcRef reference.
147157
return os.DirFS(dest), modTime, nil
148158
}
149159

160+
func storeChartLayer(path string, layer LayerData) error {
161+
data, err := io.ReadAll(layer.Reader)
162+
if err != nil {
163+
return fmt.Errorf("error reading layer[%d]: %w", layer.Index, layer.Err)
164+
}
165+
meta := new(chart.Metadata)
166+
_, err = inspectChart(data, meta)
167+
if err != nil {
168+
return fmt.Errorf("inspecting chart layer: %w", err)
169+
}
170+
filename := filepath.Join(path,
171+
fmt.Sprintf("%s-%s.tgz", meta.Name, meta.Version),
172+
)
173+
chart, err := os.Create(filename)
174+
if err != nil {
175+
return fmt.Errorf("inspecting chart layer: %w", err)
176+
}
177+
defer chart.Close()
178+
179+
_, err = chart.Write(data)
180+
return err
181+
}
182+
150183
func (a *diskCache) Delete(_ context.Context, ownerID string) error {
151184
return fsutil.DeleteReadOnlyRecursive(a.ownerIDPath(ownerID))
152185
}

internal/shared/util/image/cache_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package image
22

33
import (
44
"archive/tar"
5+
"bytes"
56
"context"
67
"errors"
78
"io"
@@ -20,6 +21,7 @@ import (
2021
ocispecv1 "github.com/opencontainers/image-spec/specs-go/v1"
2122
"github.com/stretchr/testify/assert"
2223
"github.com/stretchr/testify/require"
24+
"helm.sh/helm/v3/pkg/registry"
2325

2426
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
2527
)
@@ -144,6 +146,67 @@ func TestDiskCacheFetch(t *testing.T) {
144146
}
145147
}
146148

149+
func TestDiskCacheStore_HelmChart(t *testing.T) {
150+
const myOwner = "myOwner"
151+
myCanonicalRef := mustParseCanonical(t, "my.registry.io/ns/chart@sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03")
152+
myTaggedRef, err := reference.WithTag(reference.TrimNamed(myCanonicalRef), "test-tag")
153+
require.NoError(t, err)
154+
155+
testCases := []struct {
156+
name string
157+
ownerID string
158+
srcRef reference.Named
159+
canonicalRef reference.Canonical
160+
imgConfig ocispecv1.Image
161+
layers iter.Seq[LayerData]
162+
filterFunc func(context.Context, reference.Named, ocispecv1.Image) (archive.Filter, error)
163+
setup func(*testing.T, *diskCache)
164+
expect func(*testing.T, *diskCache, fs.FS, time.Time, error)
165+
}{
166+
{
167+
name: "returns no error if layer read contains helm chart",
168+
ownerID: myOwner,
169+
srcRef: myTaggedRef,
170+
canonicalRef: myCanonicalRef,
171+
layers: func() iter.Seq[LayerData] {
172+
testChart := mockHelmChartTgz(t,
173+
[]fileContent{
174+
{
175+
name: "testchart/Chart.yaml",
176+
content: []byte("apiVersion: v2\nname: testchart\nversion: 0.1.0"),
177+
},
178+
{
179+
name: "testchart/templates/deployment.yaml",
180+
content: []byte("kind: Deployment\napiVersion: apps/v1"),
181+
},
182+
},
183+
)
184+
return func(yield func(LayerData) bool) {
185+
yield(LayerData{Reader: bytes.NewBuffer(testChart), MediaType: registry.ChartLayerMediaType})
186+
}
187+
}(),
188+
expect: func(t *testing.T, cache *diskCache, fsys fs.FS, modTime time.Time, err error) {
189+
require.NoError(t, err)
190+
},
191+
},
192+
}
193+
for _, tc := range testCases {
194+
t.Run(tc.name, func(t *testing.T) {
195+
dc := &diskCache{
196+
basePath: t.TempDir(),
197+
filterFunc: tc.filterFunc,
198+
}
199+
if tc.setup != nil {
200+
tc.setup(t, dc)
201+
}
202+
fsys, modTime, err := dc.Store(context.Background(), tc.ownerID, tc.srcRef, tc.canonicalRef, tc.imgConfig, tc.layers)
203+
require.NotNil(t, tc.expect, "test case must include an expect function")
204+
tc.expect(t, dc, fsys, modTime, err)
205+
require.NoError(t, fsutil.DeleteReadOnlyRecursive(dc.basePath))
206+
})
207+
}
208+
}
209+
147210
func TestDiskCacheStore(t *testing.T) {
148211
const myOwner = "myOwner"
149212
myCanonicalRef := mustParseCanonical(t, "my.registry.io/ns/repo@sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03")

0 commit comments

Comments
 (0)