Skip to content

Commit

Permalink
fix: oci registry support
Browse files Browse the repository at this point in the history
Implemented the helm way: `oci://` URL as `chart` value, empty `repository`.
Repositories configured within the `repositories.yaml` file are ignored when referring to an OCI chart.

Fixes #46
  • Loading branch information
mgoltzsche committed Feb 1, 2024
1 parent b1f1ecf commit ff52c2b
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 41 deletions.
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export HELM_PLUGINS := $(BUILD_DIR)/helm-plugins

GORELEASER_VERSION ?= v1.9.2
GOLANGCI_LINT_VERSION ?= v1.51.2
# TODO: fix e2e tests and docu to make newer kpt versions work
# TODO: update kpt when panic is fixed: https://github.com/GoogleContainerTools/kpt/issues/3868
KPT_VERSION ?= v1.0.0-beta.20
KUSTOMIZE_VERSION ?= v4.5.5
BATS_VERSION = v1.7.0
Expand Down Expand Up @@ -41,8 +41,8 @@ install:
chmod +x /usr/local/bin/khelm

install-kustomize-plugin:
mkdir -p $${XDG_CONFIG_HOME:-$$HOME/.config}/kustomize/plugin/khelm.mgoltzsche.github.com/v1/chartrenderer
cp $(BUILD_DIR)/bin/khelm $${XDG_CONFIG_HOME:-$$HOME/.config}/kustomize/plugin/khelm.mgoltzsche.github.com/v1/chartrenderer/ChartRenderer
mkdir -p $${XDG_CONFIG_HOME:-$$HOME/.config}/kustomize/plugin/khelm.mgoltzsche.github.com/v2/chartrenderer
cp $(BUILD_DIR)/bin/khelm $${XDG_CONFIG_HOME:-$$HOME/.config}/kustomize/plugin/khelm.mgoltzsche.github.com/v2/chartrenderer/ChartRenderer

image: khelm
$(DOCKER) build --force-rm -t $(IMAGE) -f ./Dockerfile $(BIN_DIR)
Expand Down
11 changes: 10 additions & 1 deletion e2e/cli-tests.bats
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,13 @@ teardown() {
--debug
[ -f "$OUT_DIR/manifest.yaml" ]
grep -q myreleasex "$OUT_DIR/manifest.yaml"
}
}

@test "CLI should support oci registry" {
docker run --rm -u $(id -u):$(id -g) -v "$OUT_DIR:/out" "$IMAGE" template myreleasex oci://public.ecr.aws/karpenter/karpenter \
--version v0.27.0 \
--output /out/manifest.yaml \
--debug
[ -f "$OUT_DIR/manifest.yaml" ]
grep -q myreleasex "$OUT_DIR/manifest.yaml"
}
Empty file modified e2e/kustomize-krm-fn-tests.bats
100644 → 100755
Empty file.
8 changes: 8 additions & 0 deletions example/oci-image/generator.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
apiVersion: khelm.mgoltzsche.github.com/v2
kind: ChartRenderer
metadata:
name: karpenter
namespace: kube-system
repository: "oci://public.ecr.aws/karpenter"
chart: karpenter
version: 0.27.0
2 changes: 2 additions & 0 deletions example/oci-image/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
generators:
- generator.yaml
11 changes: 11 additions & 0 deletions pkg/helm/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry"
)

// loadChart loads chart from local or remote location
Expand All @@ -29,6 +30,8 @@ func (h *Helm) loadChart(ctx context.Context, cfg *config.ChartConfig) (*chart.C
if cfg.Repository == "" {
if fileExists {
return h.buildAndLoadLocalChart(ctx, cfg)
} else if registry.IsOCI(cfg.Chart) {
return h.loadOCIChart(ctx, cfg)
} else if l := strings.Split(cfg.Chart, "/"); len(l) == 2 && l[0] != "" && l[1] != "" && l[0] != ".." && l[0] != "." {
cfg.Repository = "@" + l[0]
cfg.Chart = l[1]
Expand All @@ -39,6 +42,14 @@ func (h *Helm) loadChart(ctx context.Context, cfg *config.ChartConfig) (*chart.C
return h.loadRemoteChart(ctx, cfg)
}

func (h *Helm) loadOCIChart(ctx context.Context, cfg *config.ChartConfig) (*chart.Chart, error) {
chartPath, err := locateChart(ctx, &cfg.LoaderConfig, nil, &h.Settings, h.Getters)
if err != nil {
return nil, err
}
return loader.Load(chartPath)
}

func (h *Helm) loadRemoteChart(ctx context.Context, cfg *config.ChartConfig) (*chart.Chart, error) {
repoURLs := map[string]struct{}{cfg.Repository: {}}
repos, err := reposForURLs(repoURLs, h.TrustAnyRepository, &h.Settings, h.Getters)
Expand Down
103 changes: 66 additions & 37 deletions pkg/helm/locate.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,70 @@ import (
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/repo"
)

// locateChart fetches the chart if not present in cache and returns its path.
// (derived from https://github.com/helm/helm/blob/fc9b46067f8f24a90b52eba31e09b31e69011e93/pkg/action/install.go#L621 -
// with efficient caching)
func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repositoryConfig, settings *cli.EnvSettings, getters getter.Providers) (string, error) {
name := cfg.Chart
name := strings.TrimSpace(cfg.Chart)
version := strings.TrimSpace(cfg.Version)
digest := "none"
chartURL := name

if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
return name, errors.Errorf("path %q not found", name)
}

repoEntry, err := repos.Get(cfg.Repository)
if err != nil {
return "", err
dl := downloader.ChartDownloader{
Out: log.Writer(),
Keyring: cfg.Keyring,
Getters: getters,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
}

cv, err := repos.ResolveChartVersion(ctx, name, cfg.Version, repoEntry.URL)
if err != nil {
return "", err
if cfg.Repository != "" {
repoEntry, err := repos.Get(cfg.Repository)
if err != nil {
return "", err
}

cv, err := repos.ResolveChartVersion(ctx, name, cfg.Version, repoEntry.URL)
if err != nil {
return "", err
}

chartURL, err = repo.ResolveReferenceURL(repoEntry.URL, cv.URLs[0])
if err != nil {
return "", errors.Wrap(err, "failed to make chart URL absolute")
}

name = cv.Name
version = cv.Version
digest = cv.Digest
dl.Options = []getter.Option{
getter.WithBasicAuth(repoEntry.Username, repoEntry.Password),
getter.WithTLSClientConfig(repoEntry.CertFile, repoEntry.KeyFile, repoEntry.CAFile),
getter.WithInsecureSkipVerifyTLS(repoEntry.InsecureSkipTLSverify),
}
}

chartURL, err := repo.ResolveReferenceURL(repoEntry.URL, cv.URLs[0])
err := ctx.Err()
if err != nil {
return "", errors.Wrap(err, "failed to make chart URL absolute")
return "", err
}

log.Printf("Downloading chart %s %s from repo %s", cfg.Chart, version, cfg.Repository)

chartCacheDir := filepath.Join(settings.RepositoryCache, "khelm")
cacheFile, err := cacheFilePath(chartURL, cv, chartCacheDir)
cacheFile, err := cacheFilePath(chartURL, name, version, digest, chartCacheDir)
if err != nil {
return "", errors.Wrap(err, "derive chart cache file")
}

if err = ctx.Err(); err != nil {
return "", err
}

if _, err = os.Stat(cacheFile); err == nil {
cacheFile, err = filepath.EvalSymlinks(cacheFile)
if err != nil {
Expand All @@ -66,27 +92,24 @@ func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repository
return cacheFile, nil
}

log.Printf("Downloading chart %s %s from repo %s", cfg.Chart, cv.Version, repoEntry.URL)

dl := downloader.ChartDownloader{
Out: log.Writer(),
Keyring: cfg.Keyring,
Getters: getters,
Options: []getter.Option{
getter.WithBasicAuth(repoEntry.Username, repoEntry.Password),
getter.WithTLSClientConfig(repoEntry.CertFile, repoEntry.KeyFile, repoEntry.CAFile),
getter.WithInsecureSkipVerifyTLS(repoEntry.InsecureSkipTLSverify),
},
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
if registry.IsOCI(name) {
registryClient, err := registry.NewClient(
registry.ClientOptEnableCache(true),
)
if err != nil {
return "", err
}
dl.RegistryClient = registryClient
dl.Options = append(dl.Options, getter.WithRegistryClient(registryClient))
}
if cfg.Verify {
dl.Verify = downloader.VerifyAlways
}

destDir := filepath.Dir(cacheFile)
destParentDir := filepath.Dir(destDir)
if err = os.MkdirAll(destParentDir, 0750); err != nil {
err = os.MkdirAll(destParentDir, 0750)
if err != nil {
return "", errors.WithStack(err)
}
tmpDestDir, err := os.MkdirTemp(destParentDir, fmt.Sprintf(".tmp-%s-", filepath.Base(destDir)))
Expand All @@ -105,9 +128,9 @@ func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repository
_ = os.RemoveAll(tmpDestDir)
}
}()
_, _, err = dl.DownloadTo(chartURL, cv.Version, tmpDestDir)
_, _, err = dl.DownloadTo(chartURL, version, tmpDestDir)
if err != nil {
err = errors.Wrapf(err, "failed to download chart %q with version %q", cfg.Chart, cv.Version)
err = errors.Wrapf(err, "failed to download chart %q with version %q", cfg.Chart, version)
return
}
err = os.Rename(tmpDestDir, destDir)
Expand All @@ -127,7 +150,7 @@ func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repository
}
}

func cacheFilePath(chartURL string, cv *repo.ChartVersion, cacheDir string) (string, error) {
func cacheFilePath(chartURL, name, version, digest, cacheDir string) (string, error) {
u, err := url.Parse(chartURL)
if err != nil {
return "", errors.Wrapf(err, "parse chart URL %q", chartURL)
Expand All @@ -141,14 +164,20 @@ func cacheFilePath(chartURL string, cv *repo.ChartVersion, cacheDir string) (str
if strings.Contains(path, "..") {
return "", errors.Errorf("get %s: path %q points outside the cache dir", chartURL, path)
}
digest := "none"
if len(cv.Digest) < 16 {
if len(digest) < 16 {
// not all the helm repository implementations populate the digest field (e.g. Nexus 3)
log.Printf("WARNING: repo index entry for chart %q does not specify a digest", cv.Name)
if digest == "" {
log.Printf("WARNING: repo index entry for chart %q does not specify a digest", name)
}
digest = "none"
} else {
digest = cv.Digest[:16]
digest = digest[:16]
}
hostSegment := strings.ReplaceAll(u.Host, ":", "_")
digestSegment := fmt.Sprintf("%s-%s-%s", cv.Name, cv.Version, digest)
return filepath.Join(cacheDir, hostSegment, filepath.Dir(path), digestSegment, filepath.Base(path)), nil
digestSegment := fmt.Sprintf("%s-%s-%s", name, version, digest)
fileName := filepath.Base(path)
if u.Scheme == "oci" {
fileName = fmt.Sprintf("%s-%s.tgz", fileName, version)
}
return filepath.Join(cacheDir, hostSegment, filepath.Dir(path), digestSegment, fileName), nil
}

0 comments on commit ff52c2b

Please sign in to comment.