Skip to content

Commit 3b36dfb

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 <ochienged@gmail.com>
1 parent 8f2f1e9 commit 3b36dfb

File tree

6 files changed

+232
-3
lines changed

6 files changed

+232
-3
lines changed

internal/operator-controller/applier/helm.go

Lines changed: 118 additions & 1 deletion
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,7 +213,120 @@ func (h *Helm) buildHelmChart(bundleFS fs.FS, ext *ocv1.ClusterExtension) (*char
209213
if err != nil {
210214
return nil, err
211215
}
212-
return h.BundleToHelmChartConverter.ToHelmChart(source.FromFS(bundleFS), ext.Spec.Namespace, watchNamespace)
216+
var result *chart.Chart
217+
contentType := bundleContentType(bundleFS)
218+
switch contentType {
219+
case HelmContent:
220+
if !features.OperatorControllerFeatureGate.Enabled(features.HelmChartSupport) {
221+
return nil, fmt.Errorf("helm chart support is not enabled")
222+
}
223+
result, err = loadChartWithEnrichments(bundleFS, WithInstallNamespace(ext.Spec.Namespace))
224+
if err != nil {
225+
return nil, fmt.Errorf("loading Helm chart; %w", err)
226+
}
227+
case BundleContent:
228+
result, err = h.BundleToHelmChartConverter.ToHelmChart(source.FromFS(bundleFS), ext.Spec.Namespace, watchNamespace)
229+
if err != nil {
230+
return nil, err
231+
}
232+
default:
233+
return nil, fmt.Errorf("unknown content found")
234+
}
235+
236+
return result, nil
237+
238+
}
239+
240+
const (
241+
HelmContent string = "helm"
242+
BundleContent string = "bundle"
243+
UnknownContent string = "unknown"
244+
)
245+
246+
func bundleContentType(bundleFS fs.FS) string {
247+
var contentType string
248+
_ = fs.WalkDir(bundleFS, ".", func(path string, f fs.DirEntry, err error) error {
249+
if err != nil {
250+
return err
251+
}
252+
253+
if !f.IsDir() {
254+
// Check if Helm chart
255+
if filepath.Dir(path) == "charts" &&
256+
filepath.Ext(f.Name()) == ".tgz" {
257+
contentType = HelmContent
258+
return fs.SkipAll
259+
}
260+
261+
// Check if registry/v1 bundle
262+
if filepath.Dir(path) == "metadata" &&
263+
f.Name() == "annotations.yaml" {
264+
contentType = BundleContent
265+
return fs.SkipAll
266+
}
267+
}
268+
269+
return nil
270+
})
271+
272+
if contentType != "" {
273+
return contentType
274+
}
275+
276+
return UnknownContent
277+
}
278+
279+
type ChartOption func(*chart.Chart)
280+
281+
func WithInstallNamespace(namespace string) ChartOption {
282+
re := regexp.MustCompile(`{{\W+\.Release\.Namespace\W+}}`)
283+
284+
return func(chrt *chart.Chart) {
285+
for i, template := range chrt.Templates {
286+
chrt.Templates[i].Data = re.ReplaceAll(template.Data, []byte(namespace))
287+
}
288+
}
289+
}
290+
291+
func loadChartWithEnrichments(bundleFS fs.FS, options ...ChartOption) (*chart.Chart, error) {
292+
chrt, err := loadPulledHelmChart(bundleFS)
293+
if err != nil {
294+
return nil, err
295+
}
296+
297+
for _, f := range options {
298+
f(chrt)
299+
}
300+
301+
return chrt, nil
302+
}
303+
304+
func loadPulledHelmChart(bundleFS fs.FS) (*chart.Chart, error) {
305+
var filename string
306+
307+
if err := fs.WalkDir(bundleFS, ".", func(path string, f fs.DirEntry, err error) error {
308+
if err != nil {
309+
return err
310+
}
311+
if strings.HasSuffix(f.Name(), ".tgz") && !f.IsDir() {
312+
filename = path
313+
return fs.SkipAll
314+
}
315+
316+
return nil
317+
}); err != nil && !errors.Is(err, fs.SkipAll) {
318+
return nil, err
319+
}
320+
321+
if filename == "" {
322+
return nil, fmt.Errorf("no helm chart found")
323+
}
324+
325+
tarball, err := fs.ReadFile(bundleFS, filename)
326+
if err != nil {
327+
return nil, fmt.Errorf("reading helm chart; %+v\n", err)
328+
}
329+
return loader.LoadArchive(bytes.NewBuffer(tarball))
213330
}
214331

215332
func (h *Helm) renderClientOnlyRelease(ctx context.Context, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, error) {

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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package image
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"io/fs"
9+
"os"
10+
"path"
11+
"path/filepath"
12+
"time"
13+
14+
"github.com/containers/image/v5/docker/reference"
15+
"github.com/containers/image/v5/types"
16+
"github.com/opencontainers/go-digest"
17+
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
18+
fsutil "github.com/operator-framework/operator-controller/internal/shared/util/fs"
19+
"helm.sh/helm/v3/pkg/registry"
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.chartsPath(ownerID, canonicalRef)
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 (a *diskCache) chartsPath(ownerID string, canonicalRef reference.Canonical) string {
58+
return filepath.Join(a.unpackPath(ownerID, canonicalRef.Digest()), "charts")
59+
}
60+
61+
func pullChart(ctx context.Context, ownerID string, img types.ImageSource, canonicalRef reference.Canonical, cache Cache, layoutDir string) (fs.FS, time.Time, error) {
62+
raw, _, err := img.GetManifest(ctx, nil)
63+
if err != nil {
64+
return nil, time.Time{}, fmt.Errorf("get OCI helm chart manifest; %w", err)
65+
}
66+
67+
chartManifest := specsv1.Manifest{}
68+
if err := json.Unmarshal(raw, &chartManifest); err != nil {
69+
return nil, time.Time{}, fmt.Errorf("unmarshaling chart manifest; %w", err)
70+
}
71+
72+
var chartDataLayerDigest digest.Digest
73+
if len(chartManifest.Layers) == 1 &&
74+
(chartManifest.Layers[0].MediaType == registry.ChartLayerMediaType) {
75+
chartDataLayerDigest = chartManifest.Layers[0].Digest
76+
}
77+
78+
filename := fmt.Sprintf("%s-%s.tgz",
79+
chartManifest.Annotations["org.opencontainers.image.title"],
80+
chartManifest.Annotations["org.opencontainers.image.version"],
81+
)
82+
83+
// Source file
84+
tarballFile := path.Join(layoutDir, "blobs", "sha256", chartDataLayerDigest.Encoded())
85+
tarball, err := os.Open(tarballFile)
86+
if err != nil {
87+
return nil, time.Time{}, fmt.Errorf("opening chart data; %w", err)
88+
}
89+
defer tarball.Close()
90+
91+
return cache.StoreChart(ownerID, filename, canonicalRef, tarball)
92+
}

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: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ func (p *ContainersImagePuller) pull(ctx context.Context, ownerID string, docker
164164
// Mount the image we just pulled
165165
//
166166
//////////////////////////////////////////////////////
167-
fsys, modTime, err = p.applyImage(ctx, ownerID, dockerRef, canonicalRef, layoutImgRef, cache, srcCtx)
167+
fsys, modTime, err = p.applyImage(ctx, ownerID, dockerRef, canonicalRef, layoutImgRef, cache, srcCtx, layoutDir)
168168
if err != nil {
169169
return nil, nil, time.Time{}, fmt.Errorf("error applying image: %w", err)
170170
}
@@ -206,7 +206,7 @@ func resolveCanonicalRef(ctx context.Context, imgRef types.ImageReference, srcCt
206206
return canonicalRef, nil
207207
}
208208

209-
func (p *ContainersImagePuller) applyImage(ctx context.Context, ownerID string, srcRef reference.Named, canonicalRef reference.Canonical, srcImgRef types.ImageReference, cache Cache, sourceContext *types.SystemContext) (fs.FS, time.Time, error) {
209+
func (p *ContainersImagePuller) applyImage(ctx context.Context, ownerID string, srcRef reference.Named, canonicalRef reference.Canonical, srcImgRef types.ImageReference, cache Cache, sourceContext *types.SystemContext, layoutDir string) (fs.FS, time.Time, error) {
210210
imgSrc, err := srcImgRef.NewImageSource(ctx, sourceContext)
211211
if err != nil {
212212
return nil, time.Time{}, fmt.Errorf("error creating image source: %w", err)
@@ -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, layoutDir)
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)