Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: oci registry support #55

Merged
merged 1 commit into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading