From 64fed6c71f08ce270c0c3b1d180e1001427a3222 Mon Sep 17 00:00:00 2001 From: Max Goltzsche Date: Fri, 24 Mar 2023 03:55:29 +0100 Subject: [PATCH] fix: oci registry support 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 --- Makefile | 6 +- e2e/cli-tests.bats | 11 ++- e2e/kustomize-krm-fn-tests.bats | 0 example/oci-image/generator.yaml | 8 +++ example/oci-image/kustomization.yaml | 2 + pkg/helm/load.go | 11 +++ pkg/helm/locate.go | 103 +++++++++++++++++---------- 7 files changed, 100 insertions(+), 41 deletions(-) mode change 100644 => 100755 e2e/kustomize-krm-fn-tests.bats create mode 100644 example/oci-image/generator.yaml create mode 100644 example/oci-image/kustomization.yaml diff --git a/Makefile b/Makefile index ac02c7f..fb3c5bc 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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) diff --git a/e2e/cli-tests.bats b/e2e/cli-tests.bats index c1951ee..3eefd64 100755 --- a/e2e/cli-tests.bats +++ b/e2e/cli-tests.bats @@ -37,4 +37,13 @@ teardown() { --debug [ -f "$OUT_DIR/manifest.yaml" ] grep -q myreleasex "$OUT_DIR/manifest.yaml" -} \ No newline at end of file +} + +@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" +} diff --git a/e2e/kustomize-krm-fn-tests.bats b/e2e/kustomize-krm-fn-tests.bats old mode 100644 new mode 100755 diff --git a/example/oci-image/generator.yaml b/example/oci-image/generator.yaml new file mode 100644 index 0000000..92dcecf --- /dev/null +++ b/example/oci-image/generator.yaml @@ -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 diff --git a/example/oci-image/kustomization.yaml b/example/oci-image/kustomization.yaml new file mode 100644 index 0000000..318ffad --- /dev/null +++ b/example/oci-image/kustomization.yaml @@ -0,0 +1,2 @@ +generators: +- generator.yaml diff --git a/pkg/helm/load.go b/pkg/helm/load.go index e9d7f6a..56fe1ae 100644 --- a/pkg/helm/load.go +++ b/pkg/helm/load.go @@ -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 @@ -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] @@ -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) diff --git a/pkg/helm/locate.go b/pkg/helm/locate.go index 516b401..87f97f5 100644 --- a/pkg/helm/locate.go +++ b/pkg/helm/locate.go @@ -14,6 +14,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" "helm.sh/helm/v3/pkg/repo" ) @@ -21,37 +22,62 @@ import ( // (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 { @@ -66,19 +92,15 @@ 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 @@ -86,7 +108,8 @@ func locateChart(ctx context.Context, cfg *config.LoaderConfig, repos repository 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))) @@ -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) @@ -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) @@ -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 }