Skip to content

Commit 39948a0

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 * extended the Cache interface to allow storing of Helm charts Signed-off-by: Edmund Ochieng <[email protected]>
1 parent 8f2f1e9 commit 39948a0

File tree

6 files changed

+188
-0
lines changed

6 files changed

+188
-0
lines changed

internal/operator-controller/applier/helm.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import (
77
"fmt"
88
"io"
99
"io/fs"
10+
"path/filepath"
11+
"regexp"
1012
"slices"
1113
"strings"
1214

1315
"helm.sh/helm/v3/pkg/action"
1416
"helm.sh/helm/v3/pkg/chart"
17+
"helm.sh/helm/v3/pkg/chart/loader"
1518
"helm.sh/helm/v3/pkg/chartutil"
1619
"helm.sh/helm/v3/pkg/postrender"
1720
"helm.sh/helm/v3/pkg/release"
@@ -26,6 +29,7 @@ import (
2629

2730
ocv1 "github.com/operator-framework/operator-controller/api/v1"
2831
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
32+
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
2933
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle/source"
3034
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
3135
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
@@ -209,9 +213,83 @@ func (h *Helm) buildHelmChart(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*char
209213
if err != nil {
210214
return nil, err
211215
}
216+
if features.OperatorControllerFeatureGate.Enabled(features.HelmChartSupport) {
217+
chart := new(chart.Chart)
218+
if IsBundleSourceHelmChart(bundleFS, chart) {
219+
return enrichChart(chart, WithInstallNamespace(ext.Spec.Namespace))
220+
}
221+
}
222+
212223
return h.BundleToHelmChartConverter.ToHelmChart(source.FromFS(bundleFS), ext.Spec.Namespace, watchNamespace)
213224
}
214225

226+
func IsBundleSourceHelmChart(bundleFS fs.FS, chart *chart.Chart) bool {
227+
var filename string
228+
if err := fs.WalkDir(bundleFS, ".",
229+
func(path string, f fs.DirEntry, err error) error {
230+
if err != nil {
231+
return err
232+
}
233+
234+
if filepath.Ext(f.Name()) == ".tgz" &&
235+
!f.IsDir() {
236+
filename = path
237+
return fs.SkipAll
238+
}
239+
240+
return nil
241+
},
242+
); err != nil &&
243+
!errors.Is(err, fs.SkipAll) {
244+
return false
245+
}
246+
247+
ch, err := readChartFS(bundleFS, filename)
248+
if err != nil {
249+
return false
250+
}
251+
*chart = *ch
252+
253+
return chart.Metadata != nil &&
254+
chart.Metadata.Name != "" &&
255+
chart.Metadata.Version != "" &&
256+
len(chart.Templates) > 0
257+
}
258+
259+
type ChartOption func(*chart.Chart)
260+
261+
func WithInstallNamespace(namespace string) ChartOption {
262+
re := regexp.MustCompile(`{{\W+\.Release\.Namespace\W+}}`)
263+
264+
return func(chrt *chart.Chart) {
265+
for i, template := range chrt.Templates {
266+
chrt.Templates[i].Data = re.ReplaceAll(template.Data, []byte(namespace))
267+
}
268+
}
269+
}
270+
271+
func enrichChart(chart *chart.Chart, options ...ChartOption) (*chart.Chart, error) {
272+
if chart != nil {
273+
for _, f := range options {
274+
f(chart)
275+
}
276+
return chart, nil
277+
}
278+
return nil, fmt.Errorf("chart can not be nil")
279+
}
280+
281+
func readChartFS(bundleFS fs.FS, filename string) (*chart.Chart, error) {
282+
if filename == "" {
283+
return nil, fmt.Errorf("chart file name was not provided")
284+
}
285+
286+
tarball, err := fs.ReadFile(bundleFS, filename)
287+
if err != nil {
288+
return nil, fmt.Errorf("reading chart %s; %+v", filename, err)
289+
}
290+
return loader.LoadArchive(bytes.NewBuffer(tarball))
291+
}
292+
215293
func (h *Helm) renderClientOnlyRelease(ctx context.Context, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, error) {
216294
// We need to get a separate action client because our work below
217295
// permanently modifies the underlying action.Configuration for ClientOnly mode.

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type LayerData struct {
2929
}
3030

3131
type Cache interface {
32+
ExtendCache
3233
Fetch(context.Context, string, reference.Canonical) (fs.FS, time.Time, error)
3334
Store(context.Context, string, reference.Named, reference.Canonical, ocispecv1.Image, iter.Seq[LayerData]) (fs.FS, time.Time, error)
3435
Delete(context.Context, string) error

internal/shared/util/image/helm.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package image
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"path/filepath"
11+
"time"
12+
13+
"github.com/containers/image/v5/docker/reference"
14+
"github.com/containers/image/v5/types"
15+
"github.com/opencontainers/go-digest"
16+
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
17+
"helm.sh/helm/v3/pkg/registry"
18+
19+
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
20+
)
21+
22+
func hasChart(imgCloser types.ImageCloser) bool {
23+
config := imgCloser.ConfigInfo()
24+
return config.MediaType == registry.ConfigMediaType
25+
}
26+
27+
type ExtendCache interface {
28+
StoreChart(string, string, reference.Canonical, io.Reader) (fs.FS, time.Time, error)
29+
}
30+
31+
func (a *diskCache) StoreChart(ownerID, filename string, canonicalRef reference.Canonical, src io.Reader) (fs.FS, time.Time, error) {
32+
dest := a.unpackPath(ownerID, canonicalRef.Digest())
33+
34+
if err := fsutil.EnsureEmptyDirectory(dest, 0700); err != nil {
35+
return nil, time.Time{}, fmt.Errorf("error ensuring empty charts directory: %w", err)
36+
}
37+
38+
// Destination file
39+
chart, err := os.Create(filepath.Join(dest, filename))
40+
if err != nil {
41+
return nil, time.Time{}, fmt.Errorf("creating chart file; %w", err)
42+
}
43+
defer chart.Close()
44+
45+
_, err = io.Copy(chart, src)
46+
if err != nil {
47+
return nil, time.Time{}, fmt.Errorf("copying chart to %s; %w", filename, err)
48+
}
49+
50+
modTime, err := fsutil.GetDirectoryModTime(dest)
51+
if err != nil {
52+
return nil, time.Time{}, fmt.Errorf("error getting mod time of unpack directory: %w", err)
53+
}
54+
return os.DirFS(filepath.Dir(dest)), modTime, nil
55+
}
56+
57+
func pullChart(ctx context.Context, ownerID string, img types.ImageSource, canonicalRef reference.Canonical, cache Cache, imgRef types.ImageReference) (fs.FS, time.Time, error) {
58+
raw, _, err := img.GetManifest(ctx, nil)
59+
if err != nil {
60+
return nil, time.Time{}, fmt.Errorf("get OCI helm chart manifest; %w", err)
61+
}
62+
63+
chartManifest := specsv1.Manifest{}
64+
if err := json.Unmarshal(raw, &chartManifest); err != nil {
65+
return nil, time.Time{}, fmt.Errorf("unmarshaling chart manifest; %w", err)
66+
}
67+
68+
var chartDataLayerDigest digest.Digest
69+
if len(chartManifest.Layers) == 1 &&
70+
(chartManifest.Layers[0].MediaType == registry.ChartLayerMediaType) {
71+
chartDataLayerDigest = chartManifest.Layers[0].Digest
72+
}
73+
74+
filename := fmt.Sprintf("%s-%s.tgz",
75+
chartManifest.Annotations["org.opencontainers.image.title"],
76+
chartManifest.Annotations["org.opencontainers.image.version"],
77+
)
78+
79+
// Source file
80+
tarball, err := os.Open(filepath.Join(
81+
imgRef.PolicyConfigurationIdentity(), "blobs",
82+
"sha256", chartDataLayerDigest.Encoded()),
83+
)
84+
if err != nil {
85+
return nil, time.Time{}, fmt.Errorf("opening chart data; %w", err)
86+
}
87+
defer tarball.Close()
88+
89+
return cache.StoreChart(ownerID, filename, canonicalRef, tarball)
90+
}

internal/shared/util/image/mocks.go

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

33
import (
44
"context"
5+
"io"
56
"io/fs"
67
"iter"
78
"time"
@@ -44,6 +45,10 @@ type MockCache struct {
4445
GarbageCollectError error
4546
}
4647

48+
func (m MockCache) StoreChart(_ string, _ string, _ reference.Canonical, _ io.Reader) (fs.FS, time.Time, error) {
49+
return m.StoreFS, m.StoreModTime, m.StoreError
50+
}
51+
4752
func (m MockCache) Fetch(_ context.Context, _ string, _ reference.Canonical) (fs.FS, time.Time, error) {
4853
return m.FetchFS, m.FetchModTime, m.FetchError
4954
}

internal/shared/util/image/pull.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,11 @@ func (p *ContainersImagePuller) applyImage(ctx context.Context, ownerID string,
224224
}
225225
}()
226226

227+
if hasChart(img) {
228+
return pullChart(ctx, ownerID, imgSrc, canonicalRef, cache, srcImgRef)
229+
}
230+
231+
// Helm charts would error when getting OCI config
227232
ociImg, err := img.OCIConfig(ctx)
228233
if err != nil {
229234
return nil, time.Time{}, err

0 commit comments

Comments
 (0)