diff --git a/.github/workflows/test_pr.yml b/.github/workflows/test_pr.yml index fd204e56..2b0dde75 100644 --- a/.github/workflows/test_pr.yml +++ b/.github/workflows/test_pr.yml @@ -35,6 +35,7 @@ jobs: - 'go.mod' - 'go.sum' - 'cmd/nelm/**' + - 'internal/**' - 'pkg/**' - 'scripts/**' lint: diff --git a/AGENTS.md b/AGENTS.md index b3da6854..c6eb9d03 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ All rules in this document are requirements — not suggestions. ALWAYS follow them. -Nelm is a Go-based Kubernetes deployment tool, which deploys Helm charts, is compatible with Helm releases, and is an alternative to Helm. Nelm is built on top of a Helm fork ([werf/3p-helm](https://github.com/werf/3p-helm)) and is also used as the deployment engine of [werf](https://github.com/werf/werf). +Nelm is a Go-based Kubernetes deployment tool, which deploys Helm charts, is compatible with Helm releases, and is an alternative to Helm. Nelm is built on top of a Helm fork and is also used as the deployment engine of [werf](https://github.com/werf/werf). ## Highest-priority rule (MANDATORY) @@ -75,7 +75,7 @@ ALWAYS use these `task` commands. NEVER use raw `go build`, `go test`, `go fmt`, - ALWAYS place tests alongside source files, not in a separate directory. - Test helpers go in `helpers_test.go` (or `helpers_ai_test.go` for AI-written helpers). - Test fixtures go in `testdata/` subdirectory next to the tests. -- Shared test helpers are in `internal/test/`. +- Shared test helpers are in `pkg/test/`. ## PR review guidelines (MANDATORY) @@ -85,7 +85,6 @@ ALWAYS use these `task` commands. NEVER use raw `go build`, `go test`, `go fmt`, ## Related repositories -- [werf/3p-helm](https://github.com/werf/3p-helm) — Helm fork. Provides chart loading, rendering, and release primitives. Changes to Helm internals go here, not in nelm. -- [werf/kubedog](https://github.com/werf/kubedog) — Kubernetes resource tracking library. Used by `internal/track/`. +- [werf/kubedog](https://github.com/werf/kubedog) — Kubernetes resource tracking library. Used by `pkg/track/`. - [werf/common-go](https://github.com/werf/common-go) — Shared Go libraries (secrets, CLI utilities, locking). - [werf/werf](https://github.com/werf/werf) — CI/CD tool that uses nelm as its deployment engine. diff --git a/README.md b/README.md index 190c68c0..d739695f 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **Nelm** is a Helm 4 alternative. It is a Kubernetes deployment tool that manages Helm Charts and deploys them to Kubernetes. It is also the deployment engine of [werf](https://github.com/werf/werf). Nelm does everything that Helm does, but better, and even quite some on top of it. Nelm is based on an improved and partially rewritten Helm codebase, to introduce: -* `terraform plan`-like capabilities; +* `terraform plan`-like capabilities with two-stage deployment support; * improved CRD management; * out-of-the-box secrets management; * advanced resource ordering capabilities; @@ -35,7 +35,7 @@ Nelm is production-ready: as the werf deployment engine, it was battle-tested ac - [Advanced resource lifecycle capabilities](#advanced-resource-lifecycle-capabilities) - [Resource state tracking](#resource-state-tracking) - [Printing logs and events during deploy](#printing-logs-and-events-during-deploy) - - [Release planning](#release-planning) + - [Release planning and two-stage deployment support](#release-planning-and-two-stage-deployment-workflow-support) - [Encrypted values and encrypted files](#encrypted-values-and-encrypted-files) - [Improved CRD management](#improved-crd-management) - [Usage](#usage) @@ -244,12 +244,23 @@ Nelm has powerful resource tracking built from the ground up, much more advanced During the deployment, Nelm finds Pods of deploying resources and periodically prints their container logs. With annotation `werf.io/show-service-messages: "true"`, resource events are also printed. Can be configured with CLI flags and annotations. -### Release planning +### Release planning and two-stage deployment workflow support -`nelm release plan install` shows exactly what's going to happen in the cluster on the next release. It shows 100% accurate diffs between current and to-be resource versions, utilizing robust dry-run Server-Side Apply instead of client-side trickery. +`nelm release plan install` shows exactly what's going to happen in the cluster on the next release. It shows diffs between the current and to-be resource versions, utilizing robust dry-run Kubernetes Server-Side Apply capabilities. ![planning](resources/images/nelm-release-plan-install.png) +To ensure that these exact changes will be applied during the release install, you can utilize a two-stage deployment workflow: + +1. **Plan:** Generate, review, and save a plan artifact using the `--save-plan` flag: +``` +nelm release plan install --save-plan=plan.gz +``` +2. **Apply:** Perform the release install rapidly using the pre-generated reviewed plan: +``` +nelm release install --use-plan=plan.gz +``` + ### Encrypted values and encrypted files `nelm chart secret` commands manage encrypted values files such as `secret-values.yaml` or encrypted arbitrary files like `secret/mysecret.txt`. These files are decrypted in-memory during templating and can be used in templates as `.Values.my.secret.value` and `{{ werf_secret_file "mysecret.txt" }}`, respectively. diff --git a/Taskfile.dist.yaml b/Taskfile.dist.yaml index d931b64b..02e9b5f5 100644 --- a/Taskfile.dist.yaml +++ b/Taskfile.dist.yaml @@ -52,7 +52,7 @@ tasks: internal: true run: once cmds: - - golangci-lint run {{if eq .fix "true"}}--fix{{end}} --build-tags="{{.tags}}" {{.CLI_ARGS}} {{.paths | default "$(find pkg/ -mindepth 1 -maxdepth 1 -type d ! -name helm -printf './%p/... ' 2>/dev/null || find pkg/ -mindepth 1 -maxdepth 1 -type d ! -name helm | xargs -I{} echo './{}/...') ./cmd/..."}} + - golangci-lint run {{if eq .fix "true"}}--fix{{end}} --build-tags="{{.tags}}" {{.CLI_ARGS}} {{.paths | default "$(find pkg/ -mindepth 1 -maxdepth 1 -type d ! -name helm -printf './%p/... ') ./cmd/..."}} vars: paths: "{{.paths}}" @@ -237,7 +237,7 @@ tasks: - task: test:ginkgo vars: paths: '{{.paths | default "./pkg ./cmd"}}' - skipPackage: "pkg/helm{{if .skipPackage}},{{.skipPackage}}{{end}}" + skipPackage: "{{.skipPackage}}" parallel: "{{.parallel}}" verify:binaries:dist:all: @@ -288,6 +288,7 @@ tasks: generate:reference: desc: "Generate CLI reference documentation with TOC." env: + # TODO(major): not supported anymore HELM_CONFIG_HOME: "~/.config/helm" HELM_CACHE_HOME: "~/.cache/helm" HELM_DATA_HOME: "~/.local/share/helm" @@ -491,8 +492,7 @@ tasks: sg scan --rule "$intern_rule" -U "{{.forkSnapshotDir}}" rm -f "$intern_rule" - rm -f "{{.forkSnapshotDir}}/cmd/helm/helm.go" - find "{{.forkSnapshotDir}}/cmd/helm" -name "*.go" -exec perl -i -p -e 's/^package main$/package helm/' {} + + rm -rf "{{.forkSnapshotDir}}/cmd" git -C "{{.forkSnapshotDir}}" add -A git -C "{{.forkSnapshotDir}}" -c user.name="sync" -c user.email="sync@local" \ @@ -504,13 +504,21 @@ tasks: git fetch "{{.forkTempRemote}}" prepared git checkout -B "{{.forkSyncBranch}}" FETCH_HEAD --no-track - stripped=0 - while IFS= read -r sync_path; do - if ! git -c core.quotePath=false ls-tree --name-only "${original_ref}" -- "{{.forkPrefix}}/${sync_path}" | grep -q .; then - git rm -rf -- "$sync_path" - stripped=$((stripped + 1)) - fi - done < <(git ls-tree --name-only HEAD) + prev_split=$(git log --grep="git-subtree-dir: {{.forkPrefix}}" --format="%b" "${original_ref}" | grep "^git-subtree-split:" | head -n1 | cut -d' ' -f2) + if [ -z "$prev_split" ]; then + echo "WARNING: no previous subtree split found for {{.forkPrefix}}, skipping file stripping" + else + stripped=0 + while IFS= read -r sync_path; do + if ! git -c core.quotePath=false ls-tree "${original_ref}" -- "{{.forkPrefix}}/${sync_path}" | grep -q .; then + if git -c core.quotePath=false ls-tree "$prev_split" -- "$sync_path" | grep -q .; then + git rm -rf -- "$sync_path" + stripped=$((stripped + 1)) + fi + fi + done < <(git -c core.quotePath=false ls-tree -r --name-only HEAD) + echo "Stripped $stripped files that were removed from our fork" + fi if ! git diff --cached --quiet; then git commit -m "sync: strip paths not kept in {{.forkPrefix}}" diff --git a/cmd/nelm/chart_dependency_download.go b/cmd/nelm/chart_dependency_download.go index 99fbc4e2..62d044d5 100644 --- a/cmd/nelm/chart_dependency_download.go +++ b/cmd/nelm/chart_dependency_download.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -31,9 +32,9 @@ func newChartDependencyDownloadCommand(ctx context.Context, afterAllCommandsBuil originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/chart_dependency_update.go b/cmd/nelm/chart_dependency_update.go index f6094e8e..bd02be74 100644 --- a/cmd/nelm/chart_dependency_update.go +++ b/cmd/nelm/chart_dependency_update.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -29,9 +30,9 @@ func newChartDependencyUpdateCommand(ctx context.Context, afterAllCommandsBuiltF originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/chart_download.go b/cmd/nelm/chart_download.go index fea72ec4..b7ff81fc 100644 --- a/cmd/nelm/chart_download.go +++ b/cmd/nelm/chart_download.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -26,9 +27,9 @@ func newChartDownloadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/chart_lint.go b/cmd/nelm/chart_lint.go index dbb77f99..e2e6e30f 100644 --- a/cmd/nelm/chart_lint.go +++ b/cmd/nelm/chart_lint.go @@ -10,7 +10,6 @@ import ( "github.com/werf/common-go/pkg/cli" "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/log" ) @@ -25,11 +24,7 @@ func newChartLintCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*co cfg := &chartLintConfig{} use := "lint [options...]" - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" - } else { - use += " [chart-dir]" - } + use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" cmd := cli.NewSubCommand( ctx, @@ -45,16 +40,10 @@ func newChartLintCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*co }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultChartLintLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultChartLintLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if len(args) > 0 { - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - cfg.Chart = args[0] - } else { - cfg.ChartDirPath = args[0] - } + cfg.Chart = args[0] } if err := action.ChartLint(ctx, cfg.ChartLintOptions); err != nil { @@ -116,13 +105,11 @@ func newChartLintCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*co return fmt.Errorf("add flag: %w", err) } - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } + if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) } // TODO: restrict allowed values diff --git a/cmd/nelm/chart_pack.go b/cmd/nelm/chart_pack.go index cdfc9283..a0feb3b2 100644 --- a/cmd/nelm/chart_pack.go +++ b/cmd/nelm/chart_pack.go @@ -9,15 +9,16 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" + "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/ts" ) func newChartPackCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { - opts := helmopts.TypeScriptOptions{} + opts := common.TypeScriptOptions{} cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { return strings.HasPrefix(c.Use, "package") })) @@ -31,9 +32,10 @@ func newChartPackCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*co originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + // TODO(major): should we do it like that everywhere, setting the context? + ctx = action.SetupLogging(cmd.Context(), lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) ctx = ts.NewContextWithTSOptions(ctx, opts) cmd.SetContext(ctx) diff --git a/cmd/nelm/chart_render.go b/cmd/nelm/chart_render.go index 837ce4e6..438000fa 100644 --- a/cmd/nelm/chart_render.go +++ b/cmd/nelm/chart_render.go @@ -10,7 +10,6 @@ import ( "github.com/werf/common-go/pkg/cli" "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/log" ) @@ -25,11 +24,7 @@ func newChartRenderCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* cfg := &chartRenderConfig{} use := "render [options...]" - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" - } else { - use += " [chart-dir]" - } + use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" cmd := cli.NewSubCommand( ctx, @@ -45,17 +40,13 @@ func newChartRenderCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultChartRenderLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultChartRenderLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) if len(args) > 0 { - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - cfg.Chart = args[0] - } else { - cfg.ChartDirPath = args[0] - } + cfg.Chart = args[0] } if _, err := action.ChartRender(ctx, cfg.ChartRenderOptions); err != nil { @@ -113,13 +104,11 @@ func newChartRenderCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* return fmt.Errorf("add flag: %w", err) } - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } + if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) } if err := cli.AddFlag(cmd, &cfg.ExtraAPIVersions, "extra-apiversions", nil, "Extra Kubernetes API versions passed to $.Capabilities.APIVersions", cli.AddFlagOptions{ @@ -150,13 +139,6 @@ func newChartRenderCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* return fmt.Errorf("add flag: %w", err) } - if err := cli.AddFlag(cmd, &cfg.ForceAdoption, "force-adoption", false, "Always adopt resources, even if they belong to a different Helm release", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - if err := cli.AddFlag(cmd, &cfg.LocalKubeVersion, "kube-version", common.DefaultLocalKubeVersion, "Kubernetes version stub for non-remote mode", cli.AddFlagOptions{ Group: mainFlagGroup, }); err != nil { diff --git a/cmd/nelm/chart_secret_file_decrypt.go b/cmd/nelm/chart_secret_file_decrypt.go index 1ced0602..02e50f53 100644 --- a/cmd/nelm/chart_secret_file_decrypt.go +++ b/cmd/nelm/chart_secret_file_decrypt.go @@ -38,7 +38,7 @@ func newChartSecretFileDecryptCommand(ctx context.Context, afterAllCommandsBuilt }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileDecryptLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileDecryptLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/chart_secret_file_edit.go b/cmd/nelm/chart_secret_file_edit.go index bd569666..b5d986c4 100644 --- a/cmd/nelm/chart_secret_file_edit.go +++ b/cmd/nelm/chart_secret_file_edit.go @@ -38,9 +38,7 @@ func newChartSecretFileEditCommand(ctx context.Context, afterAllCommandsBuiltFun }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileEditLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileEditLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) cfg.File = args[0] diff --git a/cmd/nelm/chart_secret_file_encrypt.go b/cmd/nelm/chart_secret_file_encrypt.go index 06e37ad3..be46fed5 100644 --- a/cmd/nelm/chart_secret_file_encrypt.go +++ b/cmd/nelm/chart_secret_file_encrypt.go @@ -38,7 +38,7 @@ func newChartSecretFileEncryptCommand(ctx context.Context, afterAllCommandsBuilt }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileEncryptLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretFileEncryptLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/chart_secret_key_create.go b/cmd/nelm/chart_secret_key_create.go index 0034b34f..f799b8ae 100644 --- a/cmd/nelm/chart_secret_key_create.go +++ b/cmd/nelm/chart_secret_key_create.go @@ -32,7 +32,7 @@ func newChartSecretKeyCreateCommand(ctx context.Context, afterAllCommandsBuiltFu secretCmdGroup, cli.SubCommandOptions{}, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretKeyCreateLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretKeyCreateLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/chart_secret_key_rotate.go b/cmd/nelm/chart_secret_key_rotate.go index a93a71cf..1233fa56 100644 --- a/cmd/nelm/chart_secret_key_rotate.go +++ b/cmd/nelm/chart_secret_key_rotate.go @@ -37,9 +37,7 @@ func newChartSecretKeyRotateCommand(ctx context.Context, afterAllCommandsBuiltFu }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretKeyRotateLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretKeyRotateLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if len(args) > 0 { cfg.ChartDirPath = args[0] diff --git a/cmd/nelm/chart_secret_values_file_decrypt.go b/cmd/nelm/chart_secret_values_file_decrypt.go index 39960727..a777e097 100644 --- a/cmd/nelm/chart_secret_values_file_decrypt.go +++ b/cmd/nelm/chart_secret_values_file_decrypt.go @@ -38,7 +38,7 @@ func newChartSecretValuesFileDecryptCommand(ctx context.Context, afterAllCommand }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileDecryptLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileDecryptLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/chart_secret_values_file_edit.go b/cmd/nelm/chart_secret_values_file_edit.go index dd4bdba3..1be6d539 100644 --- a/cmd/nelm/chart_secret_values_file_edit.go +++ b/cmd/nelm/chart_secret_values_file_edit.go @@ -38,9 +38,7 @@ func newChartSecretValuesFileEditCommand(ctx context.Context, afterAllCommandsBu }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileEditLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileEditLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) cfg.ValuesFile = args[0] diff --git a/cmd/nelm/chart_secret_values_file_encrypt.go b/cmd/nelm/chart_secret_values_file_encrypt.go index b9e3e07a..bc9bcc5f 100644 --- a/cmd/nelm/chart_secret_values_file_encrypt.go +++ b/cmd/nelm/chart_secret_values_file_encrypt.go @@ -38,7 +38,7 @@ func newChartSecretValuesFileEncryptCommand(ctx context.Context, afterAllCommand }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileEncryptLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultSecretValuesFileEncryptLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/chart_ts_build.go b/cmd/nelm/chart_ts_build.go index 261b2146..eb1a52f4 100644 --- a/cmd/nelm/chart_ts_build.go +++ b/cmd/nelm/chart_ts_build.go @@ -37,9 +37,7 @@ func newChartTSBuildCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[ }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), log.InfoLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), log.InfoLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if len(args) > 0 { cfg.ChartDirPath = args[0] diff --git a/cmd/nelm/chart_ts_init.go b/cmd/nelm/chart_ts_init.go index db12b3f2..46179ce4 100644 --- a/cmd/nelm/chart_ts_init.go +++ b/cmd/nelm/chart_ts_init.go @@ -37,7 +37,7 @@ func newChartTSInitCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), log.InfoLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), log.InfoLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, }) diff --git a/cmd/nelm/chart_upload.go b/cmd/nelm/chart_upload.go index 954e1888..6ba62d4d 100644 --- a/cmd/nelm/chart_upload.go +++ b/cmd/nelm/chart_upload.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -26,9 +27,9 @@ func newChartUploadCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/common_flags.go b/cmd/nelm/common_flags.go index a8dbee95..e7117689 100644 --- a/cmd/nelm/common_flags.go +++ b/cmd/nelm/common_flags.go @@ -7,7 +7,6 @@ import ( "github.com/werf/common-go/pkg/cli" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" ) func AddChartRepoConnectionFlags(cmd *cobra.Command, cfg *common.ChartRepoConnectionOptions) error { @@ -297,10 +296,6 @@ func AddKubeConnectionFlags(cmd *cobra.Command, cfg *common.KubeConnectionOption } func AddResourceValidationFlags(cmd *cobra.Command, cfg *common.ResourceValidationOptions) error { - if !featgate.FeatGateResourceValidation.Enabled() { - return nil - } - if err := cli.AddFlag(cmd, &cfg.NoResourceValidation, "no-resource-validation", false, "Disable resource validation", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, Group: resourceValidationGroup, @@ -458,14 +453,6 @@ func AddValuesFlags(cmd *cobra.Command, cfg *common.ValuesOptions) error { return fmt.Errorf("add flag: %w", err) } - if err := cli.AddFlag(cmd, &cfg.RuntimeSetJSON, "set-runtime-json", []string{}, "Set new keys in $.Runtime, where the key is the value path and the value is JSON. This is meant to be generated inside the program, so use --set-json instead, unless you know what you are doing", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: valuesFlagGroup, - NoSplitOnCommas: true, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - if err := cli.AddFlag(cmd, &cfg.ValuesFiles, "values", []string{}, "Additional values files", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, Group: valuesFlagGroup, @@ -474,11 +461,10 @@ func AddValuesFlags(cmd *cobra.Command, cfg *common.ValuesOptions) error { return fmt.Errorf("add flag: %w", err) } - // TODO(major): revise all flags in nelm/werf to make sure they are all parsed as it happens in - // Helm (see https://github.com/werf/nelm/issues/337) if err := cli.AddFlag(cmd, &cfg.ValuesSet, "set", []string{}, "Set new values, where the key is the value path and the value is the value", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, Group: valuesFlagGroup, + NoSplitOnCommas: true, }); err != nil { return fmt.Errorf("add flag: %w", err) } @@ -486,6 +472,7 @@ func AddValuesFlags(cmd *cobra.Command, cfg *common.ValuesOptions) error { if err := cli.AddFlag(cmd, &cfg.ValuesSetFile, "set-file", []string{}, "Set new values, where the key is the value path and the value is the path to the file with the value content", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, Group: valuesFlagGroup, + NoSplitOnCommas: true, }); err != nil { return fmt.Errorf("add flag: %w", err) } @@ -509,6 +496,7 @@ func AddValuesFlags(cmd *cobra.Command, cfg *common.ValuesOptions) error { if err := cli.AddFlag(cmd, &cfg.ValuesSetString, "set-string", []string{}, "Set new values, where the key is the value path and the value is the value. The value will always become a string", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, Group: valuesFlagGroup, + NoSplitOnCommas: true, }); err != nil { return fmt.Errorf("add flag: %w", err) } diff --git a/cmd/nelm/generate_reference.go b/cmd/nelm/generate_reference.go index 9824b5f2..447690c2 100644 --- a/cmd/nelm/generate_reference.go +++ b/cmd/nelm/generate_reference.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" + "github.com/samber/lo" "github.com/spf13/cobra" "github.com/spf13/pflag" @@ -70,7 +71,7 @@ func generateReferenceDoc(rootCmd *cobra.Command) string { func renderCommandMarkdown(cmd *cobra.Command, commandPath string) string { var buf bytes.Buffer - buf.WriteString(fmt.Sprintf("### %s\n\n", commandPath)) + lo.Must(fmt.Fprintf(&buf, "### %s\n\n", commandPath)) if cmd.Long != "" { buf.WriteString(util.EscapeForMarkdownPreservingCodeSpans(cmd.Long) + "\n\n") @@ -79,16 +80,16 @@ func renderCommandMarkdown(cmd *cobra.Command, commandPath string) string { } buf.WriteString("**Usage:**\n\n") - buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", cmd.UseLine())) + lo.Must(fmt.Fprintf(&buf, "```shell\n%s\n```\n\n", cmd.UseLine())) if len(cmd.Aliases) > 0 { buf.WriteString("**Aliases:** ") - buf.WriteString(fmt.Sprintf("`%s`\n\n", cmd.NameAndAliases())) + lo.Must(fmt.Fprintf(&buf, "`%s`\n\n", cmd.NameAndAliases())) } if cmd.Example != "" { buf.WriteString("**Examples:**\n\n") - buf.WriteString(fmt.Sprintf("```shell\n%s\n```\n\n", cmd.Example)) + lo.Must(fmt.Fprintf(&buf, "```shell\n%s\n```\n\n", cmd.Example)) } if cmd.HasAvailableLocalFlags() { @@ -116,7 +117,7 @@ func renderFlagsMarkdown(fset *pflag.FlagSet) string { continue } - buf.WriteString(fmt.Sprintf("**%s**\n\n", group.Title)) + lo.Must(fmt.Fprintf(&buf, "**%s**\n\n", group.Title)) for _, flag := range groupedFlags[group] { if flag.Hidden { @@ -141,10 +142,10 @@ func renderCommandsOverview(groupsByPriority []cli.CommandGroup, groupedSubComma buf.WriteString("## Commands Overview\n\n") for _, group := range groupsByPriority { - buf.WriteString(fmt.Sprintf("### %s\n\n", strings.TrimSuffix(group.Title, ":"))) + lo.Must(fmt.Fprintf(&buf, "### %s\n\n", strings.TrimSuffix(group.Title, ":"))) for _, cmdInfo := range groupedSubCommandInfos[group] { - buf.WriteString(fmt.Sprintf("- [`%s %s`](#%s) — %s\n", strings.ToLower(common.Brand), cmdInfo.commandPath, commandPathToAnchor(cmdInfo.commandPath), util.EscapeForMarkdownPreservingCodeSpans(cmdInfo.short))) + lo.Must(fmt.Fprintf(&buf, "- [`%s %s`](#%s) — %s\n", strings.ToLower(common.Brand), cmdInfo.commandPath, commandPathToAnchor(cmdInfo.commandPath), util.EscapeForMarkdownPreservingCodeSpans(cmdInfo.short))) } buf.WriteString("\n") @@ -175,8 +176,8 @@ func renderFlagMarkdown(flag *pflag.Flag) string { defValue = flag.DefValue } - buf.WriteString(fmt.Sprintf("- %s (default: `%s`)\n\n", flagName, defValue)) - buf.WriteString(fmt.Sprintf(" %s\n\n", util.EscapeForMarkdownPreservingCodeSpans(flag.Usage))) + lo.Must(fmt.Fprintf(&buf, "- %s (default: `%s`)\n\n", flagName, defValue)) + lo.Must(fmt.Fprintf(&buf, " %s\n\n", util.EscapeForMarkdownPreservingCodeSpans(flag.Usage))) return buf.String() } @@ -231,9 +232,9 @@ func renderFeatGatesMarkdown() string { buf.WriteString("Feature gates are experimental features that can be enabled via environment variables.\n\n") for _, fg := range featgate.FeatGates { - buf.WriteString(fmt.Sprintf("### %s\n\n", fg.EnvVarName())) - buf.WriteString(fmt.Sprintf("**Default:** `%v`\n\n", fg.Default())) - buf.WriteString(fmt.Sprintf("%s\n\n", util.EscapeForMarkdownPreservingCodeSpans(fg.Help))) + lo.Must(fmt.Fprintf(&buf, "### %s\n\n", fg.EnvVarName())) + lo.Must(fmt.Fprintf(&buf, "**Default:** `%v`\n\n", fg.Default())) + lo.Must(fmt.Fprintf(&buf, "%s\n\n", util.EscapeForMarkdownPreservingCodeSpans(fg.Help))) } return buf.String() diff --git a/cmd/nelm/main.go b/cmd/nelm/main.go index 6674442a..785e0f34 100644 --- a/cmd/nelm/main.go +++ b/cmd/nelm/main.go @@ -18,7 +18,7 @@ import ( "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/featgate" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -53,9 +53,9 @@ func main() { // Needed for embedding original Helm 3 commands. var err error - helmRootCmd, err = helm_v3.Init() + helmRootCmd, err = helmcmd.NewRootCmd(os.Stdout, os.Args[1:], helmcmd.SetupLogging) if err != nil { - abort(ctx, fmt.Errorf("init helm: %w", err), 1) + abort(ctx, fmt.Errorf("new helm root command: %w", err), 1) } rootCmd := NewRootCommand(ctx, afterAllCommandsBuiltFuncs) @@ -71,7 +71,7 @@ func main() { }) if unsupportedEnvVars := lo.Without(cli.FindUndefinedFlagEnvVarsInEnviron(), featGatesEnvVars...); len(unsupportedEnvVars) > 0 { - abort(ctx, fmt.Errorf("unsupported environment variable(s): %s", strings.Join(unsupportedEnvVars, ",")), 1) + log.Default.Warn(ctx, "Unsupported environment variable(s): %s", strings.Join(unsupportedEnvVars, ",")) } if err := rootCmd.ExecuteContext(ctx); err != nil { diff --git a/cmd/nelm/release.go b/cmd/nelm/release.go index 154ddf4a..74a5f58a 100644 --- a/cmd/nelm/release.go +++ b/cmd/nelm/release.go @@ -6,7 +6,6 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - "github.com/werf/nelm/pkg/featgate" ) func newReleaseCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { @@ -21,23 +20,13 @@ func newReleaseCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobr cmd.AddCommand(newReleaseInstallCommand(ctx, afterAllCommandsBuiltFuncs)) cmd.AddCommand(newReleaseRollbackCommand(ctx, afterAllCommandsBuiltFuncs)) - - if featgate.FeatGateNativeReleaseUninstall.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - cmd.AddCommand(newReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) - } else { - cmd.AddCommand(newLegacyReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) - } - + cmd.AddCommand(newReleaseUninstallCommand(ctx, afterAllCommandsBuiltFuncs)) cmd.AddCommand(newReleaseHistoryCommand(ctx, afterAllCommandsBuiltFuncs)) - - if featgate.FeatGateNativeReleaseList.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - cmd.AddCommand(newReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) - } else { - cmd.AddCommand(newLegacyReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) - } - + cmd.AddCommand(newReleaseListCommand(ctx, afterAllCommandsBuiltFuncs)) cmd.AddCommand(newReleaseGetCommand(ctx, afterAllCommandsBuiltFuncs)) cmd.AddCommand(newPlanCommand(ctx, afterAllCommandsBuiltFuncs)) + // TODO(major): in v1 don't fail on unknown env var, maybe warning (which can be disabled)? + return cmd } diff --git a/cmd/nelm/release_get.go b/cmd/nelm/release_get.go index ac8a8997..7f56d423 100644 --- a/cmd/nelm/release_get.go +++ b/cmd/nelm/release_get.go @@ -37,7 +37,7 @@ func newReleaseGetCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*c Args: cobra.MaximumNArgs(1), }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseGetLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseGetLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/release_history.go b/cmd/nelm/release_history.go index 3c3a1bf8..16414b98 100644 --- a/cmd/nelm/release_history.go +++ b/cmd/nelm/release_history.go @@ -1,38 +1,125 @@ package main import ( + "cmp" "context" - "strings" + "fmt" - "github.com/samber/lo" "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + "github.com/werf/nelm/pkg/action" + "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/log" ) +type releaseHistoryConfig struct { + action.ReleaseHistoryOptions + + LogColorMode string + LogLevel string + ReleaseName string + ReleaseNamespace string +} + func newReleaseHistoryCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { - cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { - return strings.HasPrefix(c.Use, "history") - })) + cfg := &releaseHistoryConfig{} + + cmd := cli.NewSubCommand( + ctx, + "history [options...] -n namespace -r release ", + "Show release history.", + "Show release history.", + 40, + releaseCmdGroup, + cli.SubCommandOptions{}, + func(cmd *cobra.Command, args []string) error { + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseHistoryLogLevel), action.SetupLoggingOptions{ + ColorMode: cfg.LogColorMode, + LogIsParseable: true, + }) + + if _, err := action.ReleaseHistory(ctx, cfg.ReleaseName, cfg.ReleaseNamespace, cfg.ReleaseHistoryOptions); err != nil { + return fmt.Errorf("release history: %w", err) + } + + return nil + }, + ) + + afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { + if err := AddKubeConnectionFlags(cmd, &cfg.KubeConnectionOptions); err != nil { + return fmt.Errorf("add kube connection flags: %w", err) + } + + // TODO: restrict values + if err := cli.AddFlag(cmd, &cfg.OutputFormat, "output-format", action.DefaultReleaseHistoryOutputFormat, "Result output format", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: miscFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } - cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) - cmd.Short = "Show release history." - cmd.Aliases = []string{} - cli.SetSubCommandAnnotations(cmd, 30, releaseCmdGroup) + if err := cli.AddFlag(cmd, &cfg.ReleaseName, "release", "", "The release name. Must be unique within the release namespace", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + Required: true, + ShortName: "r", + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } + + if err := cli.AddFlag(cmd, &cfg.RevisionsLimit, "revisions-limit", 0, "Maximum number of revisions to show. 0 means no limit", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } + + if err := cli.AddFlag(cmd, &cfg.ReleaseNamespace, "namespace", "", "The release namespace. Resources with no namespace will be deployed here", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + Required: true, + ShortName: "n", + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } - originalRunE := cmd.RunE - cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + // TODO: restrict allowed values + if err := cli.AddFlag(cmd, &cfg.ReleaseStorageDriver, "release-storage", "", "How releases should be stored", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, + Group: miscFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + if err := cli.AddFlag(cmd, &cfg.ReleaseStorageSQLConnection, "release-storage-sql-connection", "", "SQL connection string for MySQL release storage driver", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, + Group: miscFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } - loader.NoChartLockWarning = "" + if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, + Group: miscFlagGroup, + Type: cli.FlagTypeDir, + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } + + if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: miscFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) + } - if err := originalRunE(cmd, args); err != nil { - return err + if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultReleaseHistoryLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: miscFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) } return nil diff --git a/cmd/nelm/release_install.go b/cmd/nelm/release_install.go index 65e370cc..e587134f 100644 --- a/cmd/nelm/release_install.go +++ b/cmd/nelm/release_install.go @@ -10,7 +10,6 @@ import ( "github.com/werf/common-go/pkg/cli" "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/log" ) @@ -27,11 +26,7 @@ func newReleaseInstallCommand(ctx context.Context, afterAllCommandsBuiltFuncs ma cfg := &releaseInstallConfig{} use := "install [options...] -n namespace -r release" - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" - } else { - use += " [chart-dir]" - } + use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" cmd := cli.NewSubCommand( ctx, @@ -47,16 +42,10 @@ func newReleaseInstallCommand(ctx context.Context, afterAllCommandsBuiltFuncs ma }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseInstallLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseInstallLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if len(args) > 0 { - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - cfg.Chart = args[0] - } else { - cfg.ChartDirPath = args[0] - } + cfg.Chart = args[0] } if err := action.ReleaseInstall(ctx, cfg.ReleaseName, cfg.ReleaseNamespace, cfg.ReleaseInstallOptions); err != nil { @@ -128,13 +117,11 @@ func newReleaseInstallCommand(ctx context.Context, afterAllCommandsBuiltFuncs ma return fmt.Errorf("add flag: %w", err) } - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } + if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) } // TODO: restrict allowed values @@ -264,13 +251,6 @@ func newReleaseInstallCommand(ctx context.Context, afterAllCommandsBuiltFuncs ma return fmt.Errorf("add flag: %w", err) } - if err := cli.AddFlag(cmd, &cfg.RollbackGraphPath, "save-rollback-graph-to", "", "Save the Graphviz rollback graph to a file", cli.AddFlagOptions{ - Group: mainFlagGroup, - Type: cli.FlagTypeFile, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - if err := cli.AddFlag(cmd, &cfg.ShowSubchartNotes, "show-subchart-notes", false, "Show NOTES.txt of subcharts after the release", cli.AddFlagOptions{ Group: mainFlagGroup, }); err != nil { diff --git a/cmd/nelm/release_list.go b/cmd/nelm/release_list.go index 17fd8082..1a614569 100644 --- a/cmd/nelm/release_list.go +++ b/cmd/nelm/release_list.go @@ -32,7 +32,7 @@ func newReleaseListCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[* releaseCmdGroup, cli.SubCommandOptions{}, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseListLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseListLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/cmd/nelm/release_list_legacy.go b/cmd/nelm/release_list_legacy.go deleted file mode 100644 index 066676b6..00000000 --- a/cmd/nelm/release_list_legacy.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "context" - "strings" - - "github.com/samber/lo" - "github.com/spf13/cobra" - - "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/log" -) - -func newLegacyReleaseListCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { - cmd := lo.Must(lo.Find(helmRootCmd.Commands(), func(c *cobra.Command) bool { - return strings.HasPrefix(c.Use, "list") - })) - - cmd.LocalFlags().AddFlagSet(cmd.InheritedFlags()) - cmd.Short = "List all releases in a namespace." - cmd.Aliases = []string{} - cli.SetSubCommandAnnotations(cmd, 40, releaseCmdGroup) - - originalRunE := cmd.RunE - cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings - - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) - - loader.NoChartLockWarning = "" - - if err := originalRunE(cmd, args); err != nil { - return err - } - - return nil - } - - return cmd -} diff --git a/cmd/nelm/release_plan_install.go b/cmd/nelm/release_plan_install.go index b59ca71a..449a2e95 100644 --- a/cmd/nelm/release_plan_install.go +++ b/cmd/nelm/release_plan_install.go @@ -10,7 +10,6 @@ import ( "github.com/werf/common-go/pkg/cli" "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/log" ) @@ -27,11 +26,7 @@ func newReleasePlanInstallCommand(ctx context.Context, afterAllCommandsBuiltFunc cfg := &releasePlanInstallConfig{} use := "install [options...] -n namespace -r release" - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" - } else { - use += " [chart-dir]" - } + use += " [chart-dir|chart-repo-name/chart-name|chart-archive|chart-archive-url]" cmd := cli.NewSubCommand( ctx, @@ -47,19 +42,13 @@ func newReleasePlanInstallCommand(ctx context.Context, afterAllCommandsBuiltFunc }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleasePlanInstallLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleasePlanInstallLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if len(args) > 0 { - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - cfg.Chart = args[0] - } else { - cfg.ChartDirPath = args[0] - } + cfg.Chart = args[0] } - if err := action.ReleasePlanInstall(ctx, cfg.ReleaseName, cfg.ReleaseNamespace, cfg.ReleasePlanInstallOptions); err != nil { + if _, err := action.ReleasePlanInstall(ctx, cfg.ReleaseName, cfg.ReleaseNamespace, cfg.ReleasePlanInstallOptions); err != nil { return fmt.Errorf("release plan install: %w", err) } @@ -118,13 +107,11 @@ func newReleasePlanInstallCommand(ctx context.Context, afterAllCommandsBuiltFunc return fmt.Errorf("add flag: %w", err) } - if featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } + if err := cli.AddFlag(cmd, &cfg.ChartVersion, "chart-version", "", "Choose a remote chart version, otherwise the latest version is used", cli.AddFlagOptions{ + GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, + Group: mainFlagGroup, + }); err != nil { + return fmt.Errorf("add flag: %w", err) } // TODO: restrict allowed values @@ -142,14 +129,7 @@ func newReleasePlanInstallCommand(ctx context.Context, afterAllCommandsBuiltFunc return fmt.Errorf("add flag: %w", err) } - var desc string - if featgate.FeatGateMoreDetailedExitCodeForPlan.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - desc = "Return exit code 0 if no changes, 1 if error, 2 if resource changes planned, 3 if no resource changes planned, but release still should be installed" - } else { - desc = "Return exit code 0 if no changes, 1 if error, 2 if any changes planned" - } - - if err := cli.AddFlag(cmd, &cfg.ErrorIfChangesPlanned, "exit-code", false, desc, cli.AddFlagOptions{ + if err := cli.AddFlag(cmd, &cfg.ErrorIfChangesPlanned, "exit-code", false, "Return exit code 0 if no changes, 1 if error, 2 if resource changes planned, 3 if no resource changes planned, but release still should be installed", cli.AddFlagOptions{ Group: mainFlagGroup, }); err != nil { return fmt.Errorf("add flag: %w", err) @@ -282,14 +262,6 @@ func newReleasePlanInstallCommand(ctx context.Context, afterAllCommandsBuiltFunc return fmt.Errorf("add flag: %w", err) } - // TODO(major): get rid? - if err := cli.AddFlag(cmd, &cfg.ShowVerboseDiffs, "show-verbose-diffs", true, "Show verbose diff lines", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - if err := cli.AddFlag(cmd, &cfg.PlanArtifactPath, "save-plan", "", "Save the gzip-compressed JSON install plan to the specified file", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagLocalEnvVarRegexes, Group: mainFlagGroup, diff --git a/cmd/nelm/release_plan_show.go b/cmd/nelm/release_plan_show.go index 6942bb25..2dff62e4 100644 --- a/cmd/nelm/release_plan_show.go +++ b/cmd/nelm/release_plan_show.go @@ -37,9 +37,7 @@ func newReleasePlanShowCommand(ctx context.Context, afterAllCommandsBuiltFuncs m }, }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleasePlanShowLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleasePlanShowLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) cfg.PlanArtifactPath = args[0] @@ -73,14 +71,6 @@ func newReleasePlanShowCommand(ctx context.Context, afterAllCommandsBuiltFuncs m return fmt.Errorf("add flag: %w", err) } - // TODO(v2): get rid? - if err := cli.AddFlag(cmd, &cfg.ShowVerboseDiffs, "show-verbose-diffs", true, "Show verbose diff lines", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - if err := cli.AddFlag(cmd, &cfg.SecretKey, "secret-key", "", "Secret key for decrypting the plan artifact", cli.AddFlagOptions{ GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, Group: mainFlagGroup, diff --git a/cmd/nelm/release_rollback.go b/cmd/nelm/release_rollback.go index 4dd97d23..10cd7f59 100644 --- a/cmd/nelm/release_rollback.go +++ b/cmd/nelm/release_rollback.go @@ -37,9 +37,7 @@ func newReleaseRollbackCommand(ctx context.Context, afterAllCommandsBuiltFuncs m Args: cobra.MaximumNArgs(1), }, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseRollbackLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseRollbackLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if len(args) > 0 { var err error @@ -156,14 +154,6 @@ func newReleaseRollbackCommand(ctx context.Context, afterAllCommandsBuiltFuncs m return fmt.Errorf("add flag: %w", err) } - // TODO(major): remove this duplicated flag - if err := cli.AddFlag(cmd, &cfg.RollbackGraphPath, "save-rollback-graph-to", "", "Save the Graphviz rollback graph to a file", cli.AddFlagOptions{ - Group: mainFlagGroup, - Type: cli.FlagTypeFile, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - if err := cli.AddFlag(cmd, &cfg.RollbackGraphPath, "save-graph-to", "", "Save the Graphviz rollback graph to a file", cli.AddFlagOptions{ Group: mainFlagGroup, Type: cli.FlagTypeFile, diff --git a/cmd/nelm/release_uninstall.go b/cmd/nelm/release_uninstall.go index 80e65fa6..35200cfa 100644 --- a/cmd/nelm/release_uninstall.go +++ b/cmd/nelm/release_uninstall.go @@ -34,9 +34,7 @@ func newReleaseUninstallCommand(ctx context.Context, afterAllCommandsBuiltFuncs releaseCmdGroup, cli.SubCommandOptions{}, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseUninstallLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultReleaseUninstallLogLevel), action.SetupLoggingOptions{ColorMode: cfg.LogColorMode}) if err := action.ReleaseUninstall(ctx, cfg.ReleaseName, cfg.ReleaseNamespace, cfg.ReleaseUninstallOptions); err != nil { return fmt.Errorf("release uninstall: %w", err) diff --git a/cmd/nelm/release_uninstall_legacy.go b/cmd/nelm/release_uninstall_legacy.go deleted file mode 100644 index 27a6a156..00000000 --- a/cmd/nelm/release_uninstall_legacy.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "cmp" - "context" - "fmt" - - "github.com/spf13/cobra" - - "github.com/werf/common-go/pkg/cli" - "github.com/werf/nelm/pkg/action" - "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/log" -) - -type legacyReleaseUninstallConfig struct { - action.LegacyReleaseUninstallOptions - - LogColorMode string - LogLevel string - ReleaseName string - ReleaseNamespace string -} - -func newLegacyReleaseUninstallCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobra.Command]func(cmd *cobra.Command) error) *cobra.Command { - cfg := &legacyReleaseUninstallConfig{} - - cmd := cli.NewSubCommand( - ctx, - "uninstall [options...] -n namespace -r release", - "Uninstall a Helm Release from Kubernetes.", - "Uninstall a Helm Release from Kubernetes.", - 50, - releaseCmdGroup, - cli.SubCommandOptions{}, - func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultLegacyReleaseUninstallLogLevel), log.SetupLoggingOptions{ - ColorMode: cfg.LogColorMode, - }) - - if err := action.LegacyReleaseUninstall(ctx, cfg.ReleaseName, cfg.ReleaseNamespace, cfg.LegacyReleaseUninstallOptions); err != nil { - return fmt.Errorf("release uninstall: %w", err) - } - - return nil - }, - ) - - afterAllCommandsBuiltFuncs[cmd] = func(cmd *cobra.Command) error { - if err := AddKubeConnectionFlags(cmd, &cfg.KubeConnectionOptions); err != nil { - return fmt.Errorf("add kube connection flags: %w", err) - } - - if err := AddTrackingFlags(cmd, &cfg.TrackingOptions); err != nil { - return fmt.Errorf("add tracking flags: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.NoDeleteHooks, "no-delete-hooks", false, "Do not remove release hooks", cli.AddFlagOptions{ - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.DeleteReleaseNamespace, "delete-namespace", false, "Delete the release namespace", cli.AddFlagOptions{ - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.LogColorMode, "color-mode", common.DefaultLogColorMode, "Color mode for logs. "+allowedLogColorModesHelp(), cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: miscFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.LogLevel, "log-level", string(action.DefaultLegacyReleaseUninstallLogLevel), "Set log level. "+allowedLogLevelsHelp(), cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: miscFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.ReleaseHistoryLimit, "release-history-limit", common.DefaultReleaseHistoryLimit, "Limit the number of releases in release history. When limit is exceeded the oldest releases are deleted. Release resources are not affected", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, - Group: miscFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.NetworkParallelism, "network-parallelism", common.DefaultNetworkParallelism, "Limit of network-related tasks to run in parallel", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: performanceFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.ReleaseName, "release", "", "The release name. Must be unique within the release namespace", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - Required: true, - ShortName: "r", - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.ReleaseNamespace, "namespace", "", "The release namespace. Resources with no namespace will be deployed here", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - Required: true, - ShortName: "n", - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - // TODO: restrict allowed values - if err := cli.AddFlag(cmd, &cfg.ReleaseStorageDriver, "release-storage", "", "How releases should be stored", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, - Group: miscFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.TempDirPath, "temp-dir", "", "The directory for temporary files. By default, create a new directory in the default system directory for temporary files", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalEnvVarRegexes, - Group: miscFlagGroup, - Type: cli.FlagTypeDir, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - if err := cli.AddFlag(cmd, &cfg.Timeout, "timeout", 0, "Fail if not finished in time", cli.AddFlagOptions{ - GetEnvVarRegexesFunc: cli.GetFlagGlobalAndLocalEnvVarRegexes, - Group: mainFlagGroup, - }); err != nil { - return fmt.Errorf("add flag: %w", err) - } - - return nil - } - - return cmd -} diff --git a/cmd/nelm/repo_add.go b/cmd/nelm/repo_add.go index c2ea260c..a9cdef8c 100644 --- a/cmd/nelm/repo_add.go +++ b/cmd/nelm/repo_add.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -29,9 +30,9 @@ func newRepoAddCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobr originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/repo_login.go b/cmd/nelm/repo_login.go index 8fee821f..67d25d38 100644 --- a/cmd/nelm/repo_login.go +++ b/cmd/nelm/repo_login.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -30,9 +31,9 @@ func newRepoLoginCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*co originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/repo_logout.go b/cmd/nelm/repo_logout.go index a4ea5a61..ced85ccc 100644 --- a/cmd/nelm/repo_logout.go +++ b/cmd/nelm/repo_logout.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -30,9 +31,9 @@ func newRepoLogoutCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*c originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/repo_remove.go b/cmd/nelm/repo_remove.go index b7458519..025141e4 100644 --- a/cmd/nelm/repo_remove.go +++ b/cmd/nelm/repo_remove.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -29,9 +30,9 @@ func newRepoRemoveCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*c originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/repo_update.go b/cmd/nelm/repo_update.go index e96ff565..f878644e 100644 --- a/cmd/nelm/repo_update.go +++ b/cmd/nelm/repo_update.go @@ -8,8 +8,9 @@ import ( "github.com/spf13/cobra" "github.com/werf/common-go/pkg/cli" - helm_v3 "github.com/werf/nelm/pkg/helm/cmd/helm" + "github.com/werf/nelm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmcmd "github.com/werf/nelm/pkg/helm/pkg/cmd" "github.com/werf/nelm/pkg/log" ) @@ -30,9 +31,9 @@ func newRepoUpdateCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*c originalRunE := cmd.RunE cmd.RunE = func(cmd *cobra.Command, args []string) error { - helmSettings := helm_v3.Settings + helmSettings := helmcmd.Settings - ctx = log.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), log.SetupLoggingOptions{}) + ctx = action.SetupLogging(ctx, lo.Ternary(helmSettings.Debug, log.DebugLevel, log.InfoLevel), action.SetupLoggingOptions{}) loader.NoChartLockWarning = "" diff --git a/cmd/nelm/version.go b/cmd/nelm/version.go index d5d7b699..b263cfd5 100644 --- a/cmd/nelm/version.go +++ b/cmd/nelm/version.go @@ -32,7 +32,7 @@ func newVersionCommand(ctx context.Context, afterAllCommandsBuiltFuncs map[*cobr miscCmdGroup, cli.SubCommandOptions{}, func(cmd *cobra.Command, args []string) error { - ctx = log.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultVersionLogLevel), log.SetupLoggingOptions{ + ctx = action.SetupLogging(ctx, cmp.Or(log.Level(cfg.LogLevel), action.DefaultVersionLogLevel), action.SetupLoggingOptions{ ColorMode: cfg.LogColorMode, LogIsParseable: true, }) diff --git a/docs/reference.md b/docs/reference.md index cdfadc28..f8ac59c9 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -47,9 +47,7 @@ - [version](#version) - [Feature Gates](#feature-gates) - [NELM_FEAT_REMOTE_CHARTS](#nelm_feat_remote_charts) - - [NELM_FEAT_NATIVE_RELEASE_LIST](#nelm_feat_native_release_list) - [NELM_FEAT_PERIODIC_STACK_TRACES](#nelm_feat_periodic_stack_traces) - - [NELM_FEAT_NATIVE_RELEASE_UNINSTALL](#nelm_feat_native_release_uninstall) - [NELM_FEAT_FIELD_SENSITIVE](#nelm_feat_field_sensitive) - [NELM_FEAT_PREVIEW_V2](#nelm_feat_preview_v2) - [NELM_FEAT_CLEAN_NULL_FIELDS](#nelm_feat_clean_null_fields) @@ -67,7 +65,7 @@ - [`nelm release rollback`](#release-rollback) — Rollback to a previously deployed release\. - [`nelm release plan install`](#release-plan-install) — Plan a release install to Kubernetes\. - [`nelm release uninstall`](#release-uninstall) — Uninstall a Helm Release from Kubernetes\. -- [`nelm release list`](#release-list) — List all releases in a namespace\. +- [`nelm release list`](#release-list) — List all deployed releases\. - [`nelm release history`](#release-history) — Show release history\. - [`nelm release get`](#release-get) — Get information about a deployed release\. - [`nelm release plan show`](#release-plan-show) — Show plan artifact planned changes\. @@ -191,10 +189,6 @@ nelm release install [options...] -n namespace -r release [chart-dir] Save the install report to a file\. Var: \$NELM\_RELEASE\_INSTALL\_SAVE\_REPORT\_TO -- `--save-rollback-graph-to` (default: `""`) - - Save the Graphviz rollback graph to a file\. Var: \$NELM\_RELEASE\_INSTALL\_SAVE\_ROLLBACK\_GRAPH\_TO - - `--show-subchart-notes` (default: `false`) Show NOTES\.txt of subcharts after the release\. Var: \$NELM\_RELEASE\_INSTALL\_SHOW\_SUBCHART\_NOTES @@ -583,10 +577,6 @@ nelm release rollback [options...] -n namespace -r release [revision] Save the rollback report to a file\. Var: \$NELM\_RELEASE\_ROLLBACK\_SAVE\_REPORT\_TO -- `--save-rollback-graph-to` (default: `""`) - - Save the Graphviz rollback graph to a file\. Var: \$NELM\_RELEASE\_ROLLBACK\_SAVE\_ROLLBACK\_GRAPH\_TO - - `--timeout` (default: `0s`) Fail if not finished in time\. Vars: \$NELM\_TIMEOUT, \$NELM\_RELEASE\_ROLLBACK\_TIMEOUT @@ -859,10 +849,6 @@ nelm release plan install [options...] -n namespace -r release [chart-dir] Show verbose CRD diff lines\. Var: \$NELM\_RELEASE\_PLAN\_INSTALL\_SHOW\_VERBOSE\_CRD\_DIFFS -- `--show-verbose-diffs` (default: `true`) - - Show verbose diff lines\. Var: \$NELM\_RELEASE\_PLAN\_INSTALL\_SHOW\_VERBOSE\_DIFFS - - `--templates-allow-dns` (default: `false`) Allow performing DNS requests in templating\. Vars: \$NELM\_TEMPLATES\_ALLOW\_DNS, \$NELM\_RELEASE\_PLAN\_INSTALL\_TEMPLATES\_ALLOW\_DNS @@ -1179,18 +1165,30 @@ nelm release uninstall [options...] -n namespace -r release Delete the release namespace\. Var: \$NELM\_RELEASE\_UNINSTALL\_DELETE\_NAMESPACE +- `--delete-propagation` (default: `"Foreground"`) + + Default delete propagation strategy\. Vars: \$NELM\_DELETE\_PROPAGATION, \$NELM\_RELEASE\_UNINSTALL\_DELETE\_PROPAGATION + - `-n`, `--namespace` (default: `""`) The release namespace\. Resources with no namespace will be deployed here\. Vars: \$NELM\_NAMESPACE, \$NELM\_RELEASE\_UNINSTALL\_NAMESPACE -- `--no-delete-hooks` (default: `false`) +- `--no-remove-manual-changes` (default: `false`) - Do not remove release hooks\. Var: \$NELM\_RELEASE\_UNINSTALL\_NO\_DELETE\_HOOKS + Don't remove fields added manually to the resource in the cluster if fields aren't present in the manifest\. Vars: \$NELM\_NO\_REMOVE\_MANUAL\_CHANGES, \$NELM\_RELEASE\_UNINSTALL\_NO\_REMOVE\_MANUAL\_CHANGES - `-r`, `--release` (default: `""`) The release name\. Must be unique within the release namespace\. Vars: \$NELM\_RELEASE, \$NELM\_RELEASE\_UNINSTALL\_RELEASE +- `--save-graph-to` (default: `""`) + + Save the Graphviz uninstall graph to a file\. Var: \$NELM\_RELEASE\_UNINSTALL\_SAVE\_GRAPH\_TO + +- `--save-report-to` (default: `""`) + + Save the uninstall report to a file\. Var: \$NELM\_RELEASE\_UNINSTALL\_SAVE\_REPORT\_TO + - `--timeout` (default: `0s`) Fail if not finished in time\. Vars: \$NELM\_TIMEOUT, \$NELM\_RELEASE\_UNINSTALL\_TIMEOUT @@ -1363,6 +1361,10 @@ nelm release uninstall [options...] -n namespace -r release How releases should be stored\. Var: \$NELM\_RELEASE\_STORAGE +- `--release-storage-sql-connection` (default: `""`) + + SQL connection string for MySQL release storage driver\. Var: \$NELM\_RELEASE\_STORAGE\_SQL\_CONNECTION + - `--temp-dir` (default: `""`) The directory for temporary files\. By default, create a new directory in the default system directory for temporary files\. Var: \$NELM\_TEMP\_DIR @@ -1370,176 +1372,164 @@ nelm release uninstall [options...] -n namespace -r release ### release list +List all deployed releases\. -This command lists all of the releases for a specified namespace \(uses current namespace context if namespace not specified\)\. +**Usage:** -By default, it lists only releases that are deployed or failed\. Flags like -'\-\-uninstalled' and '\-\-all' will alter this behavior\. Such flags can be combined: -'\-\-uninstalled \-\-failed'\. +```shell +nelm release list [options...] [-n namespace] +``` -By default, items are sorted alphabetically\. Use the '\-d' flag to sort by -release date\. +**Options:** -If the \-\-filter flag is provided, it will be treated as a filter\. Filters are -regular expressions \(Perl compatible\) that are applied to the list of releases\. -Only items that match the filter will be returned\. +- `-n`, `--namespace` (default: `""`) - $ helm list --filter 'ara[a-z]+' - NAME UPDATED CHART - maudlin-arachnid 2020-06-18 14:17:46.125134977 +0000 UTC alpine-0.1.0 + The release namespace\. Query all namespaces if not specified\. Vars: \$NELM\_NAMESPACE, \$NELM\_RELEASE\_LIST\_NAMESPACE -If no results are found, 'helm list' will exit 0, but with no output \(or in -the case of no '\-q' flag, only headers\)\. -By default, up to 256 items may be returned\. To limit this, use the '\-\-max' flag\. -Setting '\-\-max' to 0 will not return all results\. Rather, it will return the -server's default, which may be much higher than 256\. Pairing the '\-\-max' -flag with the '\-\-offset' flag allows you to page through results\. +**Kubernetes connection options:** +- `--kube-api-server` (default: `""`) -**Usage:** + Kubernetes API server address\. Vars: \$NELM\_KUBE\_API\_SERVER, \$NELM\_RELEASE\_LIST\_KUBE\_API\_SERVER -```shell -nelm release list [flags] -``` +- `--kube-api-server-tls-name` (default: `""`) -**Other options:** + Server name for Kubernetes API TLS validation, if different from the hostname of Kubernetes API server\. Vars: \$NELM\_KUBE\_API\_SERVER\_TLS\_NAME, \$NELM\_RELEASE\_LIST\_KUBE\_API\_SERVER\_TLS\_NAME -- `-a`, `--all` (default: `false`) +- `--kube-auth-password` (default: `""`) - show all releases without any filter applied + Basic auth password for Kubernetes API\. Vars: \$NELM\_KUBE\_AUTH\_PASSWORD, \$NELM\_RELEASE\_LIST\_KUBE\_AUTH\_PASSWORD -- `-A`, `--all-namespaces` (default: `false`) +- `--kube-auth-provider` (default: `""`) - list releases across all namespaces + Auth provider name for authentication in Kubernetes\. Vars: \$NELM\_KUBE\_AUTH\_PROVIDER, \$NELM\_RELEASE\_LIST\_KUBE\_AUTH\_PROVIDER -- `--burst-limit` (default: `100`) +- `--kube-auth-provider-config` (default: `{}`) - client\-side default throttling limit + Auth provider config for authentication in Kubernetes API\. Vars: \$NELM\_KUBE\_AUTH\_PROVIDER\_CONFIG, \$NELM\_RELEASE\_LIST\_KUBE\_AUTH\_PROVIDER\_CONFIG -- `-d`, `--date` (default: `false`) +- `--kube-auth-username` (default: `""`) - sort by release date + Basic auth username for Kubernetes API\. Vars: \$NELM\_KUBE\_AUTH\_USERNAME, \$NELM\_RELEASE\_LIST\_KUBE\_AUTH\_USERNAME -- `--debug` (default: `false`) +- `--kube-ca` (default: `""`) - enable verbose output + Path to Kubernetes API server TLS CA file\. Vars: \$NELM\_KUBE\_CA, \$NELM\_RELEASE\_LIST\_KUBE\_CA -- `--deployed` (default: `false`) +- `--kube-ca-data` (default: `""`) - show deployed releases\. If no other is specified, this will be automatically enabled + Pass Kubernetes API server TLS CA data\. Vars: \$NELM\_KUBE\_CA\_DATA, \$NELM\_RELEASE\_LIST\_KUBE\_CA\_DATA -- `--failed` (default: `false`) +- `--kube-cert` (default: `""`) - show failed releases + Path to PEM\-encoded TLS client cert for connecting to Kubernetes API\. Vars: \$NELM\_KUBE\_CERT, \$NELM\_RELEASE\_LIST\_KUBE\_CERT -- `-f`, `--filter` (default: `""`) +- `--kube-cert-data` (default: `""`) - a regular expression \(Perl compatible\)\. Any releases that match the expression will be included in the results + Pass PEM\-encoded TLS client cert for connecting to Kubernetes API\. Vars: \$NELM\_KUBE\_CERT\_DATA, \$NELM\_RELEASE\_LIST\_KUBE\_CERT\_DATA -- `--kube-apiserver` (default: `""`) +- `--kube-config` (default: `[]`) - the address and the port for the Kubernetes API server + Kubeconfig path\(s\)\. If multiple specified, their contents are merged\. Vars: \$KUBECONFIG, \$NELM\_KUBE\_CONFIG\_\*, \$NELM\_RELEASE\_LIST\_KUBE\_CONFIG\_\* -- `--kube-as-group` (default: `[]`) +- `--kube-config-base64` (default: `""`) - group to impersonate for the operation, this flag can be repeated to specify multiple groups\. + Pass Kubeconfig file content encoded as base64\. Vars: \$NELM\_KUBE\_CONFIG\_BASE\_64, \$NELM\_RELEASE\_LIST\_KUBE\_CONFIG\_BASE\_64 -- `--kube-as-user` (default: `""`) +- `--kube-context` (default: `""`) - username to impersonate for the operation + Use specified Kubeconfig context\. Vars: \$NELM\_KUBE\_CONTEXT, \$NELM\_RELEASE\_LIST\_KUBE\_CONTEXT -- `--kube-ca-file` (default: `""`) +- `--kube-context-cluster` (default: `""`) - the certificate authority file for the Kubernetes API server connection + Use cluster from Kubeconfig for current context\. Vars: \$NELM\_KUBE\_CONTEXT\_CLUSTER, \$NELM\_RELEASE\_LIST\_KUBE\_CONTEXT\_CLUSTER -- `--kube-context` (default: `""`) +- `--kube-context-user` (default: `""`) - name of the kubeconfig context to use + Use user from Kubeconfig for current context\. Vars: \$NELM\_KUBE\_CONTEXT\_USER, \$NELM\_RELEASE\_LIST\_KUBE\_CONTEXT\_USER -- `--kube-insecure-skip-tls-verify` (default: `false`) +- `--kube-impersonate-group` (default: `[]`) - if true, the Kubernetes API server's certificate will not be checked for validity\. This will make your HTTPS connections insecure + Sets Impersonate\-Group headers when authenticating in Kubernetes\. Vars: \$NELM\_KUBE\_IMPERSONATE\_GROUP, \$NELM\_RELEASE\_LIST\_KUBE\_IMPERSONATE\_GROUP -- `--kube-tls-server-name` (default: `""`) +- `--kube-impersonate-uid` (default: `""`) - server name to use for Kubernetes API server certificate validation\. If it is not provided, the hostname used to contact the server is used + Sets Impersonate\-Uid header when authenticating in Kubernetes\. Vars: \$NELM\_KUBE\_IMPERSONATE\_UID, \$NELM\_RELEASE\_LIST\_KUBE\_IMPERSONATE\_UID -- `--kube-token` (default: `""`) +- `--kube-impersonate-user` (default: `""`) - bearer token used for authentication + Sets Impersonate\-User header when authenticating in Kubernetes\. Vars: \$NELM\_KUBE\_IMPERSONATE\_USER, \$NELM\_RELEASE\_LIST\_KUBE\_IMPERSONATE\_USER -- `--kubeconfig` (default: `""`) +- `--kube-key` (default: `""`) - path to the kubeconfig file + Path to PEM\-encoded TLS client key for connecting to Kubernetes API\. Vars: \$NELM\_KUBE\_KEY, \$NELM\_RELEASE\_LIST\_KUBE\_KEY -- `-m`, `--max` (default: `256`) +- `--kube-key-data` (default: `""`) - maximum number of releases to fetch + Pass PEM\-encoded TLS client key for connecting to Kubernetes API\. Vars: \$NELM\_KUBE\_KEY\_DATA, \$NELM\_RELEASE\_LIST\_KUBE\_KEY\_DATA -- `-n`, `--namespace` (default: `""`) +- `--kube-proxy-url` (default: `""`) - namespace scope for this request + Proxy URL to use for proxying all requests to Kubernetes API\. Vars: \$NELM\_KUBE\_PROXY\_URL, \$NELM\_RELEASE\_LIST\_KUBE\_PROXY\_URL -- `--no-headers` (default: `false`) +- `--kube-request-timeout` (default: `0s`) - don't print headers when using the default output format + Timeout for all requests to Kubernetes API\. Vars: \$NELM\_KUBE\_REQUEST\_TIMEOUT, \$NELM\_RELEASE\_LIST\_KUBE\_REQUEST\_TIMEOUT -- `--offset` (default: `0`) +- `--kube-token` (default: `""`) - next release index in the list, used to offset from start value + Bearer token for authentication in Kubernetes\. Vars: \$NELM\_KUBE\_TOKEN, \$NELM\_RELEASE\_LIST\_KUBE\_TOKEN -- `-o`, `--output` (default: `table`) +- `--kube-token-path` (default: `""`) - prints the output in the specified format\. Allowed values: table, json, yaml + Path to file with bearer token for authentication in Kubernetes\. Vars: \$NELM\_KUBE\_TOKEN\_PATH, \$NELM\_RELEASE\_LIST\_KUBE\_TOKEN\_PATH -- `--pending` (default: `false`) +- `--no-verify-kube-tls` (default: `false`) - show pending releases + Don't verify TLS certificates of Kubernetes API\. Vars: \$NELM\_NO\_VERIFY\_KUBE\_TLS, \$NELM\_RELEASE\_LIST\_NO\_VERIFY\_KUBE\_TLS -- `--qps` (default: `0`) - queries per second used when communicating with the Kubernetes API, not including bursting +**Performance options:** -- `--registry-config` (default: `"~/.config/helm/registry/config.json"`) +- `--kube-burst-limit` (default: `100`) - path to the registry config file + Burst limit for requests to Kubernetes\. Vars: \$NELM\_KUBE\_BURST\_LIMIT, \$NELM\_RELEASE\_LIST\_KUBE\_BURST\_LIMIT -- `--repository-cache` (default: `"~/.cache/helm/repository"`) +- `--kube-qps-limit` (default: `30`) - path to the file containing cached repository indexes + Queries Per Second limit for requests to Kubernetes\. Vars: \$NELM\_KUBE\_QPS\_LIMIT, \$NELM\_RELEASE\_LIST\_KUBE\_QPS\_LIMIT -- `--repository-config` (default: `"~/.config/helm/repositories.yaml"`) +- `--network-parallelism` (default: `30`) - path to the file containing repository names and URLs + Limit of network\-related tasks to run in parallel\. Vars: \$NELM\_NETWORK\_PARALLELISM, \$NELM\_RELEASE\_LIST\_NETWORK\_PARALLELISM -- `-r`, `--reverse` (default: `false`) - reverse the sort order +**Other options:** -- `-l`, `--selector` (default: `""`) +- `--color-mode` (default: `"auto"`) - Selector \(label query\) to filter on, supports '=', '==', and '\!='\.\(e\.g\. \-l key1=value1,key2=value2\)\. Works only for secret\(default\) and configmap storage backends\. + Color mode for logs\. Allowed: auto, off, on\. Vars: \$NELM\_COLOR\_MODE, \$NELM\_RELEASE\_LIST\_COLOR\_MODE -- `-q`, `--short` (default: `false`) +- `--log-level` (default: `"error"`) - output short \(quiet\) listing format + Set log level\. Allowed: silent, error, warning, info, debug, trace\. Vars: \$NELM\_LOG\_LEVEL, \$NELM\_RELEASE\_LIST\_LOG\_LEVEL -- `--superseded` (default: `false`) +- `--output-format` (default: `"table"`) - show superseded releases + Result output format\. Vars: \$NELM\_OUTPUT\_FORMAT, \$NELM\_RELEASE\_LIST\_OUTPUT\_FORMAT -- `--time-format` (default: `""`) +- `--release-storage` (default: `""`) - format time using golang time formatter\. Example: \-\-time\-format "2006\-01\-02 15:04:05Z0700" + How releases should be stored\. Var: \$NELM\_RELEASE\_STORAGE -- `--uninstalled` (default: `false`) +- `--release-storage-sql-connection` (default: `""`) - show uninstalled releases \(if 'helm uninstall \-\-keep\-history' was used\) + SQL connection string for MySQL release storage driver\. Var: \$NELM\_RELEASE\_STORAGE\_SQL\_CONNECTION -- `--uninstalling` (default: `false`) +- `--temp-dir` (default: `""`) - show releases that are currently being uninstalled + The directory for temporary files\. By default, create a new directory in the default system directory for temporary files\. Var: \$NELM\_TEMP\_DIR ### release history @@ -1843,10 +1833,6 @@ nelm release plan show [options...] plan.json Show verbose CRD diff lines\. Var: \$NELM\_RELEASE\_PLAN\_SHOW\_SHOW\_VERBOSE\_CRD\_DIFFS -- `--show-verbose-diffs` (default: `true`) - - Show verbose diff lines\. Var: \$NELM\_RELEASE\_PLAN\_SHOW\_SHOW\_VERBOSE\_DIFFS - **Other options:** @@ -2227,10 +2213,6 @@ nelm chart render [options...] [chart-dir] Extra Kubernetes API versions passed to \$\.Capabilities\.APIVersions\. Vars: \$NELM\_EXTRA\_APIVERSIONS\_\*, \$NELM\_CHART\_RENDER\_EXTRA\_APIVERSIONS\_\* -- `--force-adoption` (default: `false`) - - Always adopt resources, even if they belong to a different Helm release\. Vars: \$NELM\_FORCE\_ADOPTION, \$NELM\_CHART\_RENDER\_FORCE\_ADOPTION - - `--kube-version` (default: `"1.20.0"`) Kubernetes version stub for non\-remote mode\. Var: \$NELM\_CHART\_RENDER\_KUBE\_VERSION @@ -4099,24 +4081,12 @@ Feature gates are experimental features that can be enabled via environment vari Allow not only local, but also remote charts as an argument to cli commands\. Also adds the "\-\-chart\-version" option -### NELM_FEAT_NATIVE_RELEASE_LIST - -**Default:** `false` - -Use the native "release list" command instead of "helm list" exposed as "release list" - ### NELM_FEAT_PERIODIC_STACK_TRACES **Default:** `false` Print stack traces periodically to help with debugging deadlocks and other issues -### NELM_FEAT_NATIVE_RELEASE_UNINSTALL - -**Default:** `false` - -Use the new "release uninstall" command implementation \(not fully backwards compatible\) - ### NELM_FEAT_FIELD_SENSITIVE **Default:** `false` diff --git a/go.mod b/go.mod index bed124e9..46242a80 100644 --- a/go.mod +++ b/go.mod @@ -1,232 +1,235 @@ module github.com/werf/nelm -go 1.23.1 +go 1.25.0 require ( - github.com/BurntSushi/toml v1.3.2 + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 + github.com/BurntSushi/toml v1.6.0 github.com/DATA-DOG/go-sqlmock v1.5.2 - github.com/Masterminds/semver/v3 v3.3.1 - github.com/Masterminds/sprig/v3 v3.2.3 + github.com/Masterminds/semver/v3 v3.4.0 + github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/squirrel v1.5.4 - github.com/Masterminds/vcs v1.13.3 - github.com/alecthomas/chroma/v2 v2.15.0 + github.com/ProtonMail/go-crypto v1.4.1 + github.com/alecthomas/chroma/v2 v2.23.1 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 - github.com/aymanbagabas/go-udiff v0.2.0 + github.com/aymanbagabas/go-udiff v0.4.1 github.com/chanced/caps v1.0.2 - github.com/containerd/containerd v1.7.14 github.com/containerd/log v0.1.0 - github.com/cyphar/filepath-securejoin v0.2.5 + github.com/cyphar/filepath-securejoin v0.6.1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/distribution/distribution/v3 v3.0.0-alpha.1 - github.com/docker/cli v25.0.5+incompatible - github.com/docker/docker v25.0.5+incompatible + github.com/distribution/distribution/v3 v3.0.0 github.com/dominikbraun/graph v0.23.0 github.com/dustin/go-humanize v1.0.1 - github.com/evanphx/json-patch v5.8.0+incompatible - github.com/fluxcd/flagger v1.36.1 - github.com/foxcpp/go-mockdns v1.0.0 - github.com/go-resty/resty/v2 v2.17.1 + github.com/evanphx/json-patch v5.9.11+incompatible + github.com/evanphx/json-patch/v5 v5.9.11 + github.com/fatih/color v1.19.0 + github.com/fluxcd/cli-utils v0.37.2-flux.1 + github.com/foxcpp/go-mockdns v1.2.0 + github.com/go-resty/resty/v2 v2.17.2 github.com/gobwas/glob v0.2.3 - github.com/goccy/go-yaml v1.15.23 - github.com/gofrs/flock v0.8.1 - github.com/google/go-cmp v0.6.0 + github.com/goccy/go-yaml v1.19.2 + github.com/gofrs/flock v0.13.0 + github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/gookit/color v1.5.4 + github.com/gookit/color v1.6.0 github.com/gosimple/slug v1.15.0 github.com/gosuri/uitable v0.0.4 - github.com/hashicorp/go-multierror v1.1.1 github.com/hofstadter-io/cinful v1.0.0 - github.com/jedib0t/go-pretty/v6 v6.5.5 - github.com/jellydator/ttlcache/v3 v3.1.1 - github.com/jmoiron/sqlx v1.3.5 - github.com/lib/pq v1.10.9 - github.com/looplab/fsm v1.0.2 + github.com/jedib0t/go-pretty/v6 v6.7.8 + github.com/jellydator/ttlcache/v3 v3.4.0 + github.com/jmoiron/sqlx v1.4.0 + github.com/lib/pq v1.12.0 + github.com/looplab/fsm v1.0.3 github.com/mattn/go-shellwords v1.0.12 github.com/mitchellh/copystructure v1.2.0 - github.com/moby/term v0.5.0 - github.com/ohler55/ojg v1.26.7 - github.com/opencontainers/image-spec v1.1.0 - github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 + github.com/moby/term v0.5.2 + github.com/ohler55/ojg v1.28.1 + github.com/opencontainers/image-spec v1.1.1 github.com/pkg/errors v0.9.1 - github.com/rubenv/sql-migrate v1.6.1 - github.com/samber/lo v1.49.1 - github.com/sirupsen/logrus v1.9.3 + github.com/rubenv/sql-migrate v1.8.1 + github.com/samber/lo v1.53.0 + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 + github.com/sirupsen/logrus v1.9.4 github.com/sourcegraph/conc v0.3.0 - github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.10.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/stretchr/testify v1.11.1 github.com/tidwall/sjson v1.2.5 - github.com/wI2L/jsondiff v0.5.0 - github.com/werf/common-go v0.0.0-20251113140850-a1a98e909e9b - github.com/werf/kubedog v0.13.1-0.20260115171811-304218f24308 + github.com/wI2L/jsondiff v0.7.0 + github.com/werf/common-go v0.0.0-20260212174520-adf7d95a1579 + github.com/werf/kubedog v0.13.1-0.20260320165832-7d97aaf7aab9 github.com/werf/lockgate v0.1.1 github.com/werf/logboek v0.6.1 - github.com/xeipuuv/gojsonschema v1.2.0 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e - github.com/yannh/kubeconform v0.6.7 - golang.org/x/crypto v0.41.0 - golang.org/x/term v0.34.0 - k8s.io/api v0.29.3 - k8s.io/apiextensions-apiserver v0.29.0 - k8s.io/apimachinery v0.29.3 - k8s.io/apiserver v0.29.2 - k8s.io/cli-runtime v0.29.3 - k8s.io/client-go v0.29.3 + github.com/yannh/kubeconform v0.7.0 + golang.org/x/crypto v0.49.0 + golang.org/x/term v0.41.0 + k8s.io/api v0.35.3 + k8s.io/apiextensions-apiserver v0.35.3 + k8s.io/apimachinery v0.35.3 + k8s.io/apiserver v0.35.3 + k8s.io/cli-runtime v0.35.3 + k8s.io/client-go v0.35.3 k8s.io/klog v1.0.0 - k8s.io/klog/v2 v2.120.1 - k8s.io/kubectl v0.29.3 - oras.land/oras-go v1.2.5 - sigs.k8s.io/yaml v1.4.0 + k8s.io/klog/v2 v2.140.0 + k8s.io/kubectl v0.35.3 + oras.land/oras-go/v2 v2.6.0 + sigs.k8s.io/controller-runtime v0.23.3 + sigs.k8s.io/kustomize/kyaml v0.21.1 + sigs.k8s.io/yaml v1.6.0 ) require ( - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect - github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect - github.com/Microsoft/hcsshim v0.12.2 // indirect - github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/bshuster-repo/logrus-logstash-hook v1.1.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/cgroups/v3 v3.0.3 // indirect - github.com/containerd/continuity v0.4.3 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/distribution/reference v0.5.0 // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/djherbis/buffer v1.2.0 // indirect github.com/djherbis/nio/v3 v3.0.1 // indirect - github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect - github.com/docker/docker-credential-helpers v0.8.1 // indirect - github.com/docker/go-connections v0.5.0 // indirect - github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/docker/docker-credential-helpers v0.9.5 // indirect + github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect github.com/docker/go-metrics v0.0.1 // indirect - github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect - github.com/emicklei/go-restful/v3 v3.11.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect - github.com/fatih/color v1.16.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fluxcd/flagger v1.36.1 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.21.0 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect - github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/btree v1.1.2 // indirect - github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 // indirect - github.com/google/gofuzz v1.2.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/swag v0.25.5 // indirect + github.com/go-openapi/swag/cmdutils v0.25.5 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/fileutils v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/mangling v0.25.5 // indirect + github.com/go-openapi/swag/netutils v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.7.1 // indirect + github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect github.com/gorilla/handlers v1.5.2 // indirect github.com/gorilla/mux v1.8.1 // indirect - github.com/gorilla/websocket v1.5.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/hashicorp/golang-lru/arc/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.16 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect - github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/mattn/go-runewidth v0.0.21 // indirect github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect - github.com/moby/locker v1.0.1 // indirect - github.com/moby/spdystream v0.2.0 // indirect - github.com/moby/sys/mountinfo v0.7.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/onsi/gomega v1.39.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.19.0 // indirect - github.com/prometheus/client_model v0.6.0 // indirect - github.com/prometheus/common v0.48.0 // indirect - github.com/prometheus/procfs v0.13.0 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 // indirect - github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 // indirect - github.com/redis/go-redis/v9 v9.4.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/otlptranslator v1.0.0 // indirect + github.com/prometheus/procfs v0.20.1 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect + github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 // indirect + github.com/redis/go-redis/v9 v9.18.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 // indirect - github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect - github.com/shopspring/decimal v1.3.1 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/cast v1.6.0 // indirect - github.com/tidwall/gjson v1.17.0 // indirect - github.com/tidwall/match v1.1.1 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect - go.opentelemetry.io/otel/exporters/prometheus v0.44.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/sdk v1.24.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.opentelemetry.io/proto/otlp v1.1.0 // indirect - go.starlark.net v0.0.0-20231121155337-90ade8b19d09 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 // indirect + go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/prometheus v0.64.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 // indirect + go.opentelemetry.io/otel/log v0.18.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/sdk/log v0.18.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.35.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa // indirect - google.golang.org/grpc v1.62.1 // indirect - google.golang.org/protobuf v1.35.1 // indirect - gopkg.in/evanphx/json-patch.v5 v5.8.0 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.52.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.35.0 // indirect + golang.org/x/time v0.15.0 // indirect + golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 // indirect + google.golang.org/grpc v1.79.3 // indirect + google.golang.org/protobuf v1.36.11 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.1 // indirect - k8s.io/component-base v0.29.3 // indirect - k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 // indirect - k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/kustomize/api v0.16.0 // indirect - sigs.k8s.io/kustomize/kyaml v0.16.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + k8s.io/component-base v0.35.3 // indirect + k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9 // indirect + k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.21.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) -replace github.com/spf13/cobra => github.com/andremueller/cobra v0.0.0-20241025091859-0d550c15a8a4 // remove when merged: https://github.com/spf13/cobra/pull/2167 +replace github.com/spf13/cobra => github.com/werf/3p-cobra v0.0.0-20260403075225-552c82797324 // adds EnableErrorOnUnknownSubcommand, not yet in upstream diff --git a/go.sum b/go.sum index fb3acad9..a6da4310 100644 --- a/go.sum +++ b/go.sum @@ -1,138 +1,123 @@ -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= -github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.12.2 h1:AcXy+yfRvrx20g9v7qYaJv5Rh+8GaHOS6b8G6Wx/nKs= -github.com/Microsoft/hcsshim v0.12.2/go.mod h1:RZV12pcHCXQ42XnlQ3pz6FZfmrC1C+R4gaOHhRNML1g= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= -github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM= +github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= -github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= +github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= +github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/andremueller/cobra v0.0.0-20241025091859-0d550c15a8a4 h1:behIQV+NveRm5cqXAfvYDEA2AkjwE8rdJjyA7YT5XlM= -github.com/andremueller/cobra v0.0.0-20241025091859-0d550c15a8a4/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774 h1:HrMVYtly2IVqg9EBooHsakQ256ueojP7QuG32K71X/U= github.com/avelino/slugify v0.0.0-20180501145920-855f152bd774/go.mod h1:5wi5YYOpfuAKwL5XLFYopbgIl/v7NZxaJpa/4X6yFKE= -github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= -github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= +github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o= +github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4= github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0= -github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= -github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chanced/caps v1.0.2 h1:RELvNN4lZajqSXJGzPaU7z8B4LK2+o2Oc/upeWdgMOA= github.com/chanced/caps v1.0.2/go.mod h1:SJhRzeYLKJ3OmzyQXhdZ7Etj7lqqWoPtQ1zcSJRtQjs= -github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= -github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= -github.com/containerd/containerd v1.7.14 h1:H/XLzbnGuenZEGK+v0RkwTdv2u1QFAruMe5N0GNPJwA= -github.com/containerd/containerd v1.7.14/go.mod h1:YMC9Qt5yzNqXx/fO4j/5yYVIHXSRrlB3H7sxkUTvspg= -github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7bEZ9Su8= -github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= -github.com/containerd/errdefs v0.1.0 h1:m0wCRBiu1WJT/Fr+iOoQHMQS/eP5myQ8lCv4Dz5ZURM= -github.com/containerd/errdefs v0.1.0/go.mod h1:YgWiiHtLmSeBrvpw+UfPijzbLaB77mEG1WwJTDETIV0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= -github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.0.0-alpha.1 h1:jn7I1gvjOvmLztH1+1cLiUFud7aeJCIQcgzugtwjyJo= -github.com/distribution/distribution/v3 v3.0.0-alpha.1/go.mod h1:LCp4JZp1ZalYg0W/TN05jarCQu+h4w7xc7ZfQF4Y/cY= -github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= -github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= -github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= -github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v25.0.5+incompatible h1:3Llw3kcE1gOScEojA247iDD+p1l9hHeC7H3vf3Zd5fk= -github.com/docker/cli v25.0.5+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v25.0.5+incompatible h1:UmQydMduGkrD5nQde1mecF/YnSbTOaPeFIeP5C4W+DE= -github.com/docker/docker v25.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqYyMHOp5vfsD1807oKo= -github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= -github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= +github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4= +github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.11.2 h1:1onLa9DcsMYO9P+CXaL0dStDqQ2EHHXLiz+BtnqkLAU= -github.com/emicklei/go-restful/v3 v3.11.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/evanphx/json-patch v5.8.0+incompatible h1:1Av9pn2FyxPdvrWNQszj1g6D6YthSmvCfcN6SYclTJg= -github.com/evanphx/json-patch v5.8.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= -github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= +github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fluxcd/cli-utils v0.37.2-flux.1 h1:tQ588ghtRN+E+kHq415FddfqA9v4brn/1WWgrP6rQR0= +github.com/fluxcd/cli-utils v0.37.2-flux.1/go.mod h1:LcWSu1NYET8d8U7O326RhEm5JkQXCMK6ITu4G1CT02c= github.com/fluxcd/flagger v1.36.1 h1:X2PumtNwZz9YSGaOtZLFm2zAKLgHhFkbNv8beg7ifyc= github.com/fluxcd/flagger v1.36.1/go.mod h1:qmtLsxheVDTI8XeCaXUxW5UCmfcSKnY9fizG9NmW/Fk= -github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= -github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= @@ -142,72 +127,85 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= -github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= -github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= -github.com/go-resty/resty/v2 v2.17.1 h1:x3aMpHK1YM9e4va/TMDRlusDDoZiQ+ViDu/WpA6xTM4= -github.com/go-resty/resty/v2 v2.17.1/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= -github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/swag v0.25.5 h1:pNkwbUEeGwMtcgxDr+2GBPAk4kT+kJ+AaB+TMKAg+TU= +github.com/go-openapi/swag v0.25.5/go.mod h1:B3RT6l8q7X803JRxa2e59tHOiZlX1t8viplOcs9CwTA= +github.com/go-openapi/swag/cmdutils v0.25.5 h1:yh5hHrpgsw4NwM9KAEtaDTXILYzdXh/I8Whhx9hKj7c= +github.com/go-openapi/swag/cmdutils v0.25.5/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/fileutils v0.25.5 h1:B6JTdOcs2c0dBIs9HnkyTW+5gC+8NIhVBUwERkFhMWk= +github.com/go-openapi/swag/fileutils v0.25.5/go.mod h1:V3cT9UdMQIaH4WiTrUc9EPtVA4txS0TOmRURmhGF4kc= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/mangling v0.25.5 h1:hyrnvbQRS7vKePQPHHDso+k6CGn5ZBs5232UqWZmJZw= +github.com/go-openapi/swag/mangling v0.25.5/go.mod h1:6hadXM/o312N/h98RwByLg088U61TPGiltQn71Iw0NY= +github.com/go-openapi/swag/netutils v0.25.5 h1:LZq2Xc2QI8+7838elRAaPCeqJnHODfSyOa7ZGfxDKlU= +github.com/go-openapi/swag/netutils v0.25.5/go.mod h1:lHbtmj4m57APG/8H7ZcMMSWzNqIQcu0RFiXrPUara14= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-resty/resty/v2 v2.17.2 h1:FQW5oHYcIlkCNrMD2lloGScxcHJ0gkjshV3qcQAyHQk= +github.com/go-resty/resty/v2 v2.17.2/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo= -github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49 h1:0VpGH+cDhbDtdcweoyCVsF3fhN8kejK6rFe/2FFX2nU= -github.com/google/gnostic-models v0.6.9-0.20230804172637-c7be7c783f49/go.mod h1:BkkQ4L1KS1xMt2aWSPStnn55ChGC0DPOn2FQYj+f25M= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= -github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw= +github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= +github.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0= +github.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E= +github.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA= +github.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs= github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= @@ -216,19 +214,14 @@ github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= -github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= github.com/hashicorp/golang-lru/arc/v2 v2.0.7 h1:QxkVTxwColcduO+LP7eJO56r2hFiG8zEbfAAzRv52KQ= github.com/hashicorp/golang-lru/arc/v2 v2.0.7/go.mod h1:Pe7gBlGdc8clY5LJ0LpJXMt5AmgmWNH1g+oFFVUHOEc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= @@ -237,114 +230,92 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hofstadter-io/cinful v1.0.0 h1:G/kZ/iwM0EyTyEtdE4UyLNLOVNSSHVs1cW0DC7uoxmE= github.com/hofstadter-io/cinful v1.0.0/go.mod h1:VySLSoBPf5gTFEeumOhl8I2cjspiJAB3/XGgrivpUZI= -github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jedib0t/go-pretty/v6 v6.5.5 h1:PpIU8lOjxvVYGGKule0QxxJfNysUSbC9lggQU2cpZJc= -github.com/jedib0t/go-pretty/v6 v6.5.5/go.mod h1:5LQIxa52oJ/DlDSLv0HEkWOFMDGoWkJb9ss5KqPpJBg= -github.com/jellydator/ttlcache/v3 v3.1.1 h1:RCgYJqo3jgvhl+fEWvjNW8thxGWsgxi+TPhRir1Y9y8= -github.com/jellydator/ttlcache/v3 v3.1.1/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= +github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= +github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= -github.com/looplab/fsm v1.0.2 h1:f0kdMzr4CRpXtaKKRUxwLYJ7PirTdwrtNumeLN+mDx8= -github.com/looplab/fsm v1.0.2/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/looplab/fsm v1.0.3 h1:qtxBsa2onOs0qFOtkqwf5zE0uP0+Te+wlIvXctPKpcw= +github.com/looplab/fsm v1.0.3/go.mod h1:PmD3fFvQEIsjMEfvZdrCDZ6y8VwKTwWNjlpEr6IKPO4= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= +github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= -github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= -github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= -github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/ohler55/ojg v1.26.7 h1:yZLS2xlZF/qk5LHM4LFhxxTDyMgZl+46Z6p7wQm8KAU= -github.com/ohler55/ojg v1.26.7/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= -github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= -github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= -github.com/onsi/gomega v1.36.0 h1:Pb12RlruUtj4XUuPUqeEWc6j5DkVVVA49Uf6YLfC95Y= -github.com/onsi/gomega v1.36.0/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/ohler55/ojg v1.28.1 h1:Xy93DelhLSZNeWv8GPKtP6qMqkUlZlAxBP/AQcC5RfY= +github.com/ohler55/ojg v1.28.1/go.mod h1:/Y5dGWkekv9ocnUixuETqiL58f+5pAsUfg5P8e7Pa2o= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -356,91 +327,91 @@ github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjz github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= -github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/otlptranslator v1.0.0 h1:s0LJW/iN9dkIH+EnhiD3BlkkP5QVIUVEoIwkU+A6qos= +github.com/prometheus/otlptranslator v1.0.0/go.mod h1:vRYWnXvI6aWGpsdY/mOT/cbeVRBlPWtBNDb7kGR3uKM= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o= -github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= -github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk= -github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rubenv/sql-migrate v1.6.1 h1:bo6/sjsan9HaXAsNxYP/jCEDUGibHp8JmOBw7NTGRos= -github.com/rubenv/sql-migrate v1.6.1/go.mod h1:tPzespupJS0jacLfhbwto/UjSX+8h2FdWB7ar+QlHa0= +github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= +github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU= +github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo= +github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE= +github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10= +github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= +github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew= -github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= -github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= -github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/wI2L/jsondiff v0.5.0 h1:RRMTi/mH+R2aXcPe1VYyvGINJqQfC3R+KSEakuU1Ikw= -github.com/wI2L/jsondiff v0.5.0/go.mod h1:qqG6hnK0Lsrz2BpIVCxWiK9ItsBCpIZQiv0izJjOZ9s= -github.com/werf/common-go v0.0.0-20251113140850-a1a98e909e9b h1:58850oFrnw5Jy5YaB8QifXz75qpGotfx6qqZ9Q2my1A= -github.com/werf/common-go v0.0.0-20251113140850-a1a98e909e9b/go.mod h1:MXS0JR9zut+oR9oEM8PEkdXXoEbKDILTmWopt0z1eZs= -github.com/werf/kubedog v0.13.1-0.20260115171811-304218f24308 h1:ee55f/lNya8V9jCBsQWDhvOw6y1fB0uysop8te9aUcM= -github.com/werf/kubedog v0.13.1-0.20260115171811-304218f24308/go.mod h1:gu4EY4hxtiYVDy5o6WE2lRZS0YWqrOV0HS//GTYyrUE= +github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ= +github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM= +github.com/werf/3p-cobra v0.0.0-20260403075225-552c82797324 h1:aqEM5aboMpBfsILjaxxRKhGFv9rGtNcd5YzMUDyVX+U= +github.com/werf/3p-cobra v0.0.0-20260403075225-552c82797324/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/werf/common-go v0.0.0-20260212174520-adf7d95a1579 h1:LojMRgEoMNrUnfsbDG5GT4M5HXC9LAYH+5DWBY3p4uU= +github.com/werf/common-go v0.0.0-20260212174520-adf7d95a1579/go.mod h1:MXS0JR9zut+oR9oEM8PEkdXXoEbKDILTmWopt0z1eZs= +github.com/werf/kubedog v0.13.1-0.20260320165832-7d97aaf7aab9 h1:N+XKTPiXT5pf5lxThhaQQPARLUpZTlYJeMNoNtn+540= +github.com/werf/kubedog v0.13.1-0.20260320165832-7d97aaf7aab9/go.mod h1:93L6aIdpj7iIhL30Obkv7bWgUyTeuxas1ijtzjmyb4Q= github.com/werf/lockgate v0.1.1 h1:S400JFYjtWfE4i4LY9FA8zx0fMdfui9DPrBiTciCrx4= github.com/werf/lockgate v0.1.1/go.mod h1:0yIFSLq9ausy6ejNxF5uUBf/Ib6daMAfXuCaTMZJzIE= github.com/werf/logboek v0.6.1 h1:oEe6FkmlKg0z0n80oZjLplj6sXcBeLleCkjfOOZEL2g= github.com/werf/logboek v0.6.1/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -452,201 +423,219 @@ github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yannh/kubeconform v0.6.7 h1:kIvjeiMSU0+/GY48+U9GmJZdGmoej4dArYvv3BfvlyA= -github.com/yannh/kubeconform v0.6.7/go.mod h1:lcx9py+svwYnKXiy146zVstEToiTuTu4rMzdXXfsyVc= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yannh/kubeconform v0.7.0 h1:ZFfniR8VChrWQxaxTUGnNrxw8RIDkjVBrjdhXSamwjw= +github.com/yannh/kubeconform v0.7.0/go.mod h1:oHO1wjM16sTRW6s41HJUox+tD69qOTE5ZVQ9HeqX+xM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/exporters/autoexport v0.46.1 h1:ysCfPZB9AjUlMa1UHYup3c9dAOCMQX/6sxSfPBUoxHw= -go.opentelemetry.io/contrib/exporters/autoexport v0.46.1/go.mod h1:ha0aiYm+DOPsLHjh0zoQ8W8sLT+LJ58J3j47lGpSLrU= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0 h1:jd0+5t/YynESZqsSyPz+7PAFdEop0dlN0+PkyHYo8oI= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.44.0/go.mod h1:U707O40ee1FpQGyhvqnzmCJm1Wh6OX6GGBVn0E6Uyyk= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0 h1:bflGWrfYyuulcdxf14V6n9+CoQcu5SAAdHmDPAJnlps= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.44.0/go.mod h1:qcTO4xHAxZLaLxPd60TdE88rxtItPHgHWqOhOGRr0as= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM= -go.opentelemetry.io/otel/exporters/prometheus v0.44.0 h1:08qeJgaPC0YEBu2PQMbqU3rogTlyzpjhCI2b58Yn00w= -go.opentelemetry.io/otel/exporters/prometheus v0.44.0/go.mod h1:ERL2uIeBtg4TxZdojHUwzZfIFlUIjZtxubT5p4h1Gjg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 h1:dEZWPjVN22urgYCza3PXRUGEyCB++y1sAqm6guWFesk= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0/go.mod h1:sTt30Evb7hJB/gEk27qLb1+l9n4Tb8HvHkR0Wx3S6CU= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0 h1:VhlEQAPp9R1ktYfrPk5SOryw1e9LDDTZCbIPFrho0ec= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.21.0/go.mod h1:kB3ufRbfU+CQ4MlUcqtW8Z7YEOBeK2DJ6CmR5rYYF3E= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw= -go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg= -go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= -go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= -go.starlark.net v0.0.0-20231121155337-90ade8b19d09 h1:hzy3LFnSN8kuQK8h9tHl4ndF6UruMj47OqwqsS+/Ai4= -go.starlark.net v0.0.0-20231121155337-90ade8b19d09/go.mod h1:LcLNIzVOMp4oV+uusnpk+VU+SzXaJakUuBjoCSWH5dM= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0 h1:dkBzNEAIKADEaFnuESzcXvpd09vxvDZsOjx11gjUqLk= +go.opentelemetry.io/contrib/bridges/prometheus v0.67.0/go.mod h1:Z5RIwRkZgauOIfnG5IpidvLpERjhTninpP1dTG2jTl4= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0 h1:4fnRcNpc6YFtG3zsFw9achKn3XgmxPxuMuqIL5rE8e8= +go.opentelemetry.io/contrib/exporters/autoexport v0.67.0/go.mod h1:qTvIHMFKoxW7HXg02gm6/Wofhq5p3Ib/A/NNt1EoBSQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0 h1:deI9UQMoGFgrg5iLPgzueqFPHevDl+28YKfSpPTI6rY= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.18.0/go.mod h1:PFx9NgpNUKXdf7J4Q3agRxMs3Y07QhTCVipKmLsMKnU= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0 h1:icqq3Z34UrEFk2u+HMhTtRsvo7Ues+eiJVjaJt62njs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0/go.mod h1:W2m8P+d5Wn5kipj4/xmbt9uMqezEKfBjzVJadfABSBE= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0 h1:H7O6RlGOMTizyl3R08Kn5pdM06bnH8oscSj7o11tmLA= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0/go.mod h1:mBFWu/WOVDkWWsR7Tx7h6EpQB8wsv7P0Yrh0Pb7othc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0 h1:g0LRDXMX/G1SEZtK8zl8Chm4K6GBwRkjPKE36LxiTYs= +go.opentelemetry.io/otel/exporters/prometheus v0.64.0/go.mod h1:UrgcjnarfdlBDP3GjDIJWe6HTprwSazNjwsI+Ru6hro= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0 h1:KJVjPD3rcPb98rIs3HznyJlrfx9ge5oJvxxlGR+P/7s= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.18.0/go.mod h1:K3kRa2ckmHWQaTWQdPRHc7qGXASuVuoEQXzrvlA98Ws= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= +go.opentelemetry.io/otel/log v0.18.0 h1:XgeQIIBjZZrliksMEbcwMZefoOSMI1hdjiLEiiB0bAg= +go.opentelemetry.io/otel/log v0.18.0/go.mod h1:KEV1kad0NofR3ycsiDH4Yjcoj0+8206I6Ox2QYFSNgI= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/log v0.18.0 h1:n8OyZr7t7otkeTnPTbDNom6rW16TBYGtvyy2Gk6buQw= +go.opentelemetry.io/otel/sdk/log v0.18.0/go.mod h1:C0+wxkTwKpOCZLrlJ3pewPiiQwpzycPI/u6W0Z9fuYk= +go.opentelemetry.io/otel/sdk/log/logtest v0.18.0 h1:l3mYuPsuBx6UKE47BVcPrZoZ0q/KER57vbj2qkgDLXA= +go.opentelemetry.io/otel/sdk/log/logtest v0.18.0/go.mod h1:7cHtiVJpZebB3wybTa4NG+FUo5NPe3PROz1FqB0+qdw= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= +go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa h1:Jt1XW5PaLXF1/ePZrznsh/aAUvI7Adfc3LY1dAKlzRs= -google.golang.org/genproto/googleapis/api v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:K4kfzHtI0kqWA79gecJarFtDn/Mls+GxQcg3Zox91Ac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa h1:RBgMaUMP+6soRkik4VoN8ojR2nex2TqZwjSSogic+eo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240325203815-454cdb8f5daa/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI= +google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/evanphx/json-patch.v5 v5.8.0 h1:A8QNKkaxzza4Ubx7N23Yav3OstJhP8KYRZbk98mZsFo= -gopkg.in/evanphx/json-patch.v5 v5.8.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= -gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -k8s.io/api v0.29.3 h1:2ORfZ7+bGC3YJqGpV0KSDDEVf8hdGQ6A03/50vj8pmw= -k8s.io/api v0.29.3/go.mod h1:y2yg2NTyHUUkIoTC+phinTnEa3KFM6RZ3szxt014a80= -k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= -k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= -k8s.io/apimachinery v0.29.3 h1:2tbx+5L7RNvqJjn7RIuIKu9XTsIZ9Z5wX2G22XAa5EU= -k8s.io/apimachinery v0.29.3/go.mod h1:hx/S4V2PNW4OMg3WizRrHutyB5la0iCUbZym+W0EQIU= -k8s.io/apiserver v0.29.2 h1:+Z9S0dSNr+CjnVXQePG8TcBWHr3Q7BmAr7NraHvsMiQ= -k8s.io/apiserver v0.29.2/go.mod h1:B0LieKVoyU7ykQvPFm7XSdIHaCHSzCzQWPFa5bqbeMQ= -k8s.io/cli-runtime v0.29.3 h1:r68rephmmytoywkw2MyJ+CxjpasJDQY7AGc3XY2iv1k= -k8s.io/cli-runtime v0.29.3/go.mod h1:aqVUsk86/RhaGJwDhHXH0jcdqBrgdF3bZWk4Z9D4mkM= -k8s.io/client-go v0.29.3 h1:R/zaZbEAxqComZ9FHeQwOh3Y1ZUs7FaHKZdQtIc2WZg= -k8s.io/client-go v0.29.3/go.mod h1:tkDisCvgPfiRpxGnOORfkljmS+UrW+WtXAy2fTvXJB0= -k8s.io/component-base v0.29.3 h1:Oq9/nddUxlnrCuuR2K/jp6aflVvc0uDvxMzAWxnGzAo= -k8s.io/component-base v0.29.3/go.mod h1:Yuj33XXjuOk2BAaHsIGHhCKZQAgYKhqIxIjIr2UXYio= +k8s.io/api v0.35.3 h1:pA2fiBc6+N9PDf7SAiluKGEBuScsTzd2uYBkA5RzNWQ= +k8s.io/api v0.35.3/go.mod h1:9Y9tkBcFwKNq2sxwZTQh1Njh9qHl81D0As56tu42GA4= +k8s.io/apiextensions-apiserver v0.35.3 h1:2fQUhEO7P17sijylbdwt0nBdXP0TvHrHj0KeqHD8FiU= +k8s.io/apiextensions-apiserver v0.35.3/go.mod h1:tK4Kz58ykRpwAEkXUb634HD1ZAegEElktz/B3jgETd8= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.3 h1:D2eIcfJ05hEAEewoSDg+05e0aSRwx8Y4Agvd/wiomUI= +k8s.io/apiserver v0.35.3/go.mod h1:JI0n9bHYzSgIxgIrfe21dbduJ9NHzKJ6RchcsmIKWKY= +k8s.io/cli-runtime v0.35.3 h1:UZq4ipNimtzBmhN7PPKbfAdqo8quK0H0UdGl6qAQnqI= +k8s.io/cli-runtime v0.35.3/go.mod h1:O7MUmCqcKSd5xI+O5X7/pRkB5l0O2NIhOdUVwbHLXu4= +k8s.io/client-go v0.35.3 h1:s1lZbpN4uI6IxeTM2cpdtrwHcSOBML1ODNTCCfsP1pg= +k8s.io/client-go v0.35.3/go.mod h1:RzoXkc0mzpWIDvBrRnD+VlfXP+lRzqQjCmKtiwZ8Q9c= +k8s.io/component-base v0.35.3 h1:mbKbzoIMy7JDWS/wqZobYW1JDVRn/RKRaoMQHP9c4P0= +k8s.io/component-base v0.35.3/go.mod h1:IZ8LEG30kPN4Et5NeC7vjNv5aU73ku5MS15iZyvyMYk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910 h1:1Rp/XEKP5uxPs6QrsngEHAxBjaAR78iJRiJq5Fi7LSU= -k8s.io/kube-openapi v0.0.0-20240105020646-a37d4de58910/go.mod h1:Pa1PvrP7ACSkuX6I7KYomY6cmMA0Tx86waBhDUgoKPw= -k8s.io/kubectl v0.29.3 h1:RuwyyIU42MAISRIePaa8Q7A3U74Q9P4MoJbDFz9o3us= -k8s.io/kubectl v0.29.3/go.mod h1:yCxfY1dbwgVdEt2zkJ6d5NNLOhhWgTyrqACIoFhpdd4= -k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= -k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= -oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g= -sigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c= -sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0= -sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9 h1:Sztf7ESG9tAXRW/ACJZjrj5jhdOUqS2KFRQT+CTvu78= +k8s.io/kube-openapi v0.0.0-20260319004828-5883c5ee87b9/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/kubectl v0.35.3 h1:1KqSYXk/sodU7VeDvK6atX2kAGUZd2QTeR5K7Hb9r9w= +k8s.io/kubectl v0.35.3/go.mod h1:GPHxZqRe+u/i3gTBoVQHeIyq2NilfNPj9hDWeuN3x5s= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5 h1:kBawHLSnx/mYHmRnNUf9d4CpjREbeZuxoSGOX/J+aYM= +k8s.io/utils v0.0.0-20260319190234-28399d86e0b5/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.21.1 h1:lzqbzvz2CSvsjIUZUBNFKtIMsEw7hVLJp0JeSIVmuJs= +sigs.k8s.io/kustomize/api v0.21.1/go.mod h1:f3wkKByTrgpgltLgySCntrYoq5d3q7aaxveSagwTlwI= +sigs.k8s.io/kustomize/kyaml v0.21.1 h1:IVlbmhC076nf6foyL6Taw4BkrLuEsXUXNpsE+ScX7fI= +sigs.k8s.io/kustomize/kyaml v0.21.1/go.mod h1:hmxADesM3yUN2vbA5z1/YTBnzLJ1dajdqpQonwBL1FQ= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/pkg/action/chart_lint.go b/pkg/action/chart_lint.go index 4de9866e..1de25d00 100644 --- a/pkg/action/chart_lint.go +++ b/pkg/action/chart_lint.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -13,7 +12,7 @@ import ( "github.com/werf/nelm/pkg/chart" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/plan" @@ -38,8 +37,6 @@ type ChartLintOptions struct { // ChartAppVersion overrides the appVersion field in Chart.yaml. // Used to set application version metadata without modifying the chart file. ChartAppVersion string - // ChartDirPath is deprecated (TODO v2: remove). Use Chart instead. - ChartDirPath string // ChartProvenanceKeyring is the path to a keyring file containing public keys // used to verify chart provenance signatures. Used with signed charts for security. ChartProvenanceKeyring string @@ -84,7 +81,7 @@ type ChartLintOptions struct { IgnoreBundleJS bool // LegacyChartType specifies the chart type for legacy compatibility. // Used internally for backward compatibility with werf integration. - LegacyChartType helmopts.ChartType + LegacyChartType common.LegacyChartType // LegacyExtraValues provides additional values programmatically. // Used internally for backward compatibility with werf integration. LegacyExtraValues map[string]interface{} @@ -157,16 +154,7 @@ func ChartLint(ctx context.Context, opts ChartLintOptions) error { var clientFactory *kube.ClientFactory if opts.Remote { - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: opts.ReleaseNamespace, // TODO: unset it everywhere }) @@ -207,8 +195,8 @@ func ChartLint(ctx context.Context, opts ChartLintOptions) error { return fmt.Errorf("construct release storage: %w", err) } - helmOptions := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ + helmOptions := common.HelmOptions{ + ChartLoadOpts: common.ChartLoadOptions{ ChartAppVersion: opts.ChartAppVersion, ChartType: opts.LegacyChartType, DefaultChartAPIVersion: opts.DefaultChartAPIVersion, @@ -242,7 +230,7 @@ func ChartLint(ctx context.Context, opts ChartLintOptions) error { if prevRelease != nil { newRevision = prevRelease.Version + 1 - prevReleaseFailed = prevRelease.IsStatusFailed() + prevReleaseFailed = prevRelease.Info.Status == helmreleasestatus.StatusFailed } else { newRevision = 1 } @@ -404,9 +392,7 @@ func applyChartLintOptionsDefaults(opts ChartLintOptions, currentDir, homeDir st opts.ValuesOptions.ApplyDefaults() opts.SecretValuesOptions.ApplyDefaults(currentDir) - if opts.Chart == "" && opts.ChartDirPath != "" { - opts.Chart = opts.ChartDirPath - } else if opts.ChartDirPath == "" && opts.Chart == "" { + if opts.Chart == "" { opts.Chart = currentDir } diff --git a/pkg/action/chart_render.go b/pkg/action/chart_render.go index 8e1c5f0c..284b78ac 100644 --- a/pkg/action/chart_render.go +++ b/pkg/action/chart_render.go @@ -18,7 +18,6 @@ import ( "github.com/werf/nelm/pkg/chart" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/release" @@ -40,8 +39,6 @@ type ChartRenderOptions struct { // ChartAppVersion overrides the appVersion field in Chart.yaml. // Used to set application version metadata without modifying the chart file. ChartAppVersion string - // ChartDirPath is deprecated (TODO v2: remove). Use Chart instead. - ChartDirPath string // ChartProvenanceKeyring is the path to a keyring file containing public keys // used to verify chart provenance signatures. Used with signed charts for security. ChartProvenanceKeyring string @@ -74,14 +71,11 @@ type ChartRenderOptions struct { // ExtraRuntimeAnnotations are additional annotations to add to resources at runtime. // TODO(major): remove or implement custom logic for this field. ExtraRuntimeAnnotations map[string]string - // ForceAdoption is currently unused in chart rendering. - // TODO(major): remove this useless field. - ForceAdoption bool // IgnoreBundleJS, when true, ignores the existing bundle.js and rebuilds it from TypeScript sources. IgnoreBundleJS bool // LegacyChartType specifies the chart type for legacy compatibility. // Used internally for backward compatibility with werf integration. - LegacyChartType helmopts.ChartType + LegacyChartType common.LegacyChartType // LegacyExtraValues provides additional values programmatically. // Used internally for backward compatibility with werf integration. LegacyExtraValues map[string]interface{} @@ -165,16 +159,7 @@ func ChartRender(ctx context.Context, opts ChartRenderOptions) (*ChartRenderResu var clientFactory *kube.ClientFactory if opts.Remote { - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: opts.ReleaseNamespace, // TODO: unset it everywhere }) @@ -215,8 +200,8 @@ func ChartRender(ctx context.Context, opts ChartRenderOptions) (*ChartRenderResu return nil, fmt.Errorf("construct release storage: %w", err) } - helmOptions := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ + helmOptions := common.HelmOptions{ + ChartLoadOpts: common.ChartLoadOptions{ ChartAppVersion: opts.ChartAppVersion, ChartType: opts.LegacyChartType, DefaultChartAPIVersion: opts.DefaultChartAPIVersion, @@ -406,9 +391,7 @@ func applyChartRenderOptionsDefaults(opts ChartRenderOptions, currentDir, homeDi opts.ValuesOptions.ApplyDefaults() opts.SecretValuesOptions.ApplyDefaults(currentDir) - if opts.Chart == "" && opts.ChartDirPath != "" { - opts.Chart = opts.ChartDirPath - } else if opts.ChartDirPath == "" && opts.Chart == "" { + if opts.Chart == "" { opts.Chart = currentDir } diff --git a/pkg/action/chart_ts_build.go b/pkg/action/chart_ts_build.go index 6660156d..26368b0d 100644 --- a/pkg/action/chart_ts_build.go +++ b/pkg/action/chart_ts_build.go @@ -13,9 +13,9 @@ import ( "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/featgate" - helmchart "github.com/werf/nelm/pkg/helm/pkg/chart" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + v2chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/ts" ) @@ -45,22 +45,29 @@ func ChartTSBuild(ctx context.Context, opts ChartTSBuildOptions) error { log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render("Run bundle for ")+"%s", absPath) - helmOpts := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ - ChartType: helmopts.ChartTypeChart, + helmOpts := common.HelmOptions{ + ChartLoadOpts: common.ChartLoadOptions{ + ChartType: common.LegacyChartTypeChart, }, } - chart, err := loader.Load(absPath, helmOpts) + ctx = common.ContextWithHelmOptions(ctx, helmOpts) + + loadedChart, err := loader.Load(ctx, absPath) if err != nil { return fmt.Errorf("load chart: %w", err) } + chart, ok := loadedChart.(*v2chart.Chart) + if !ok { + return fmt.Errorf("unsupported chart type %T", loadedChart) + } + if err = ts.BundleChartsRecursive(ctx, chart, absPath, true, opts.DenoBinaryPath); err != nil { return fmt.Errorf("process chart: %w", err) } - bundles := lo.Filter(chart.Raw, func(file *helmchart.File, _ int) bool { + bundles := lo.Filter(chart.Raw, func(file *chartcommon.File, _ int) bool { return strings.Contains(file.Name, common.ChartTSBundleFile) }) diff --git a/pkg/action/chart_ts_init.go b/pkg/action/chart_ts_init.go index 531f177e..fb078573 100644 --- a/pkg/action/chart_ts_init.go +++ b/pkg/action/chart_ts_init.go @@ -8,7 +8,7 @@ import ( "path/filepath" "github.com/werf/nelm/pkg/featgate" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/ts" ) @@ -36,7 +36,7 @@ func ChartTSInit(ctx context.Context, opts ChartTSInitOptions) error { if opts.ChartName != "" { chartName = opts.ChartName } else { - meta, err := chartutil.LoadChartfile(filepath.Join(absPath, "Chart.yaml")) + meta, err := util.LoadChartfile(filepath.Join(absPath, "Chart.yaml")) if err != nil { return fmt.Errorf("load Chart.yaml: %w", err) } diff --git a/pkg/action/common.go b/pkg/action/common.go index 52e7ff1b..2a9d585f 100644 --- a/pkg/action/common.go +++ b/pkg/action/common.go @@ -16,12 +16,12 @@ import ( "github.com/samber/lo" "github.com/xo/terminfo" + "github.com/werf/kubedog/pkg/dyntracker/logstore" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/kubedog/pkg/informer" - "github.com/werf/kubedog/pkg/trackers/dyntracker/logstore" - "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/plan" @@ -73,16 +73,15 @@ var syntaxHighlightTheme = fmt.Sprintf(` `, syntaxHighlightThemeName) -// TODO(major): Version > APIVersion as string "v3" -type releaseReportV3 struct { - Version int `json:"version,omitempty"` - Release string `json:"release,omitempty"` - Namespace string `json:"namespace,omitempty"` - Revision int `json:"revision,omitempty"` - Status helmrelease.Status `json:"status,omitempty"` - CompletedOperations []string `json:"completedOperations,omitempty"` - CanceledOperations []string `json:"canceledOperations,omitempty"` - FailedOperations []string `json:"failedOperations,omitempty"` +type ReleaseReportV3 struct { + APIVersion string `json:"apiVersion,omitempty"` + Release string `json:"release,omitempty"` + Namespace string `json:"namespace,omitempty"` + Revision int `json:"revision,omitempty"` + Status helmreleasestatus.Status `json:"status,omitempty"` + CompletedOperations []string `json:"completedOperations,omitempty"` + CanceledOperations []string `json:"canceledOperations,omitempty"` + FailedOperations []string `json:"failedOperations,omitempty"` } type runFailureInstallPlanOptions struct { @@ -127,7 +126,7 @@ func printNotes(ctx context.Context, notes string) { }) } -func printReport(ctx context.Context, report *releaseReportV3) { +func printReport(ctx context.Context, report *ReleaseReportV3) { if totalOpsLen := len(report.CompletedOperations) + len(report.CanceledOperations) + len(report.FailedOperations); totalOpsLen == 0 { return } @@ -233,7 +232,7 @@ func savePlanAsDot(plan *plan.Plan, path string) error { return nil } -func saveReport(reportPath string, report *releaseReportV3) error { +func saveReport(reportPath string, report *ReleaseReportV3) error { reportByte, err := json.MarshalIndent(report, "", "\t") if err != nil { return fmt.Errorf("marshal report: %w", err) diff --git a/pkg/plan/plan_artifact.go b/pkg/action/plan_artifact.go similarity index 91% rename from pkg/plan/plan_artifact.go rename to pkg/action/plan_artifact.go index 367a8b6b..f75eda75 100644 --- a/pkg/plan/plan_artifact.go +++ b/pkg/action/plan_artifact.go @@ -1,4 +1,4 @@ -package plan +package action import ( "compress/gzip" @@ -13,8 +13,9 @@ import ( "github.com/werf/common-go/pkg/secrets_manager" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/helm/pkg/release" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/log" + "github.com/werf/nelm/pkg/plan" ) const PlanArtifactSchemeVersion = "v1" @@ -32,11 +33,11 @@ type PlanArtifact struct { type PlanArtifactData struct { Options common.ReleaseInstallRuntimeOptions `json:"options"` - Changes []*ResourceChange `json:"changes"` - Plan *Plan `json:"plan"` - Release *release.Release `json:"release"` - InstallableResourceInfos []*InstallableResourceInfo `json:"installableResourceInfos"` - ReleaseInfos []*ReleaseInfo `json:"releaseInfos"` + Changes []*plan.ResourceChange `json:"changes"` + Plan *plan.Plan `json:"plan"` + Release *helmrelease.Release `json:"release"` + InstallableResourceInfos []*plan.InstallableResourceInfo `json:"installableResourceInfos"` + ReleaseInfos []*plan.ReleaseInfo `json:"releaseInfos"` } type PlanArtifactRelease struct { diff --git a/pkg/action/release_get.go b/pkg/action/release_get.go index 8939dc18..8c7f57d4 100644 --- a/pkg/action/release_get.go +++ b/pkg/action/release_get.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "os" - "path/filepath" "strings" "time" @@ -14,9 +13,10 @@ import ( "github.com/samber/lo" "github.com/werf/nelm/pkg/common" + chartcommonutil "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/release" @@ -59,22 +59,20 @@ type ReleaseGetOptions struct { TempDirPath string } -type ReleaseGetResultV1 struct { - APIVersion string `json:"apiVersion"` - Release *ReleaseGetResultRelease `json:"release"` - Chart *ReleaseGetResultChart `json:"chart"` - Notes string `json:"notes,omitempty"` - Values map[string]interface{} `json:"values,omitempty"` - // TODO(major): Join Hooks and Resources together as ResourceSpecs? - Hooks []map[string]interface{} `json:"hooks,omitempty"` - Resources []map[string]interface{} `json:"resources,omitempty"` +type ReleaseGetResultV2 struct { + APIVersion string `json:"apiVersion"` + Release *ReleaseGetResultRelease `json:"release"` + Chart *ReleaseGetResultChart `json:"chart"` + Notes string `json:"notes,omitempty"` + Values map[string]interface{} `json:"values,omitempty"` + ResourceSpecs []*spec.ResourceSpec `json:"resourceSpecs,omitempty"` } type ReleaseGetResultRelease struct { Name string `json:"name"` Namespace string `json:"namespace"` Revision int `json:"revision"` - Status helmrelease.Status `json:"status"` + Status helmreleasestatus.Status `json:"status"` DeployedAt *ReleaseGetResultDeployedAt `json:"deployedAt"` Annotations map[string]string `json:"annotations"` StorageLabels map[string]string `json:"storageLabels"` @@ -92,7 +90,7 @@ type ReleaseGetResultChart struct { } // Retrieves detailed information about the Helm release from the cluster. -func ReleaseGet(ctx context.Context, releaseName, releaseNamespace string, opts ReleaseGetOptions) (*ReleaseGetResultV1, error) { +func ReleaseGet(ctx context.Context, releaseName, releaseNamespace string, opts ReleaseGetOptions) (*ReleaseGetResultV2, error) { homeDir, err := os.UserHomeDir() if err != nil { return nil, fmt.Errorf("get home directory: %w", err) @@ -103,16 +101,7 @@ func ReleaseGet(ctx context.Context, releaseName, releaseNamespace string, opts return nil, fmt.Errorf("build release get options: %w", err) } - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: releaseNamespace, // TODO: unset it everywhere }) @@ -165,13 +154,13 @@ func ReleaseGet(ctx context.Context, releaseName, releaseNamespace string, opts } } - values, err := chartutil.CoalesceValues(rel.Chart, rel.Config) + values, err := chartcommonutil.CoalesceValues(rel.Chart, rel.Config) if err != nil { return nil, fmt.Errorf("coalesce release values: %w", err) } - result := &ReleaseGetResultV1{ - APIVersion: "v1", + result := &ReleaseGetResultV2{ + APIVersion: "v2", Chart: &ReleaseGetResultChart{ Name: rel.Chart.Name(), Version: rel.Chart.Metadata.Version, @@ -198,13 +187,7 @@ func ReleaseGet(ctx context.Context, releaseName, releaseNamespace string, opts return nil, fmt.Errorf("convert release to resource specs: %w", err) } - for _, res := range resSpecs { - if spec.IsHook(res.Annotations) { - result.Hooks = append(result.Hooks, res.Unstruct.Object) - } else { - result.Resources = append(result.Resources, res.Unstruct.Object) - } - } + result.ResourceSpecs = append(result.ResourceSpecs, resSpecs...) if opts.OutputNoPrint { return result, nil diff --git a/pkg/action/release_history.go b/pkg/action/release_history.go new file mode 100644 index 00000000..d46fb5b1 --- /dev/null +++ b/pkg/action/release_history.go @@ -0,0 +1,323 @@ +package action + +import ( + "context" + "encoding/json" + "fmt" + "os" + "sort" + "strings" + "time" + + "github.com/goccy/go-yaml" + "github.com/gookit/color" + prtable "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" + + "github.com/werf/nelm/pkg/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" + "github.com/werf/nelm/pkg/kube" + "github.com/werf/nelm/pkg/log" + "github.com/werf/nelm/pkg/release" +) + +const ( + DefaultReleaseHistoryLogLevel = log.ErrorLevel + DefaultReleaseHistoryOutputFormat = common.OutputFormatTable +) + +type ReleaseHistoryOptions struct { + common.KubeConnectionOptions + + // OutputFormat specifies the output format for the release history. + // Valid values: "table" (default), "yaml", "json". + // Defaults to DefaultReleaseHistoryOutputFormat (table) if not specified. + OutputFormat string + // OutputNoPrint, when true, suppresses printing the output and only returns the result data structure. + // Useful when calling this programmatically. + OutputNoPrint bool + // ReleaseStorageDriver specifies how release metadata is stored in Kubernetes. + // Valid values: "secret" (default), "configmap", "sql". + // Defaults to "secret" if not specified or set to "default". + ReleaseStorageDriver string + // ReleaseStorageSQLConnection is the SQL connection string when using SQL storage driver. + // Only used when ReleaseStorageDriver is "sql". + ReleaseStorageSQLConnection string + // RevisionsLimit limits the number of revisions returned. 0 means no limit. + RevisionsLimit int + // TempDirPath is the directory for temporary files during the operation. + // A temporary directory is created automatically if not specified. + TempDirPath string +} + +type ReleaseHistoryResultV1 struct { + APIVersion string `json:"apiVersion"` + Releases []*ReleaseHistoryResultRelease `json:"releases"` +} + +type ReleaseHistoryResultRelease struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Revision int `json:"revision"` + Status helmreleasestatus.Status `json:"status"` + DeployedAt *ReleaseHistoryResultDeployedAt `json:"deployedAt"` + Annotations map[string]string `json:"annotations"` + Chart *ReleaseHistoryResultChart `json:"chart"` +} + +type ReleaseHistoryResultDeployedAt struct { + Human string `json:"human"` + Unix int `json:"unix"` +} + +type ReleaseHistoryResultChart struct { + Name string `json:"name"` + Version string `json:"version"` + AppVersion string `json:"appVersion"` +} + +// Lists Helm release history from the cluster. +func ReleaseHistory(ctx context.Context, releaseName, releaseNamespace string, opts ReleaseHistoryOptions) (*ReleaseHistoryResultV1, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("get home directory: %w", err) + } + + opts, err = applyReleaseHistoryOptionsDefaults(opts, homeDir) + if err != nil { + return nil, fmt.Errorf("build release history options: %w", err) + } + + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ + KubeConnectionOptions: opts.KubeConnectionOptions, + KubeContextNamespace: releaseNamespace, // TODO: unset it everywhere + }) + if err != nil { + return nil, fmt.Errorf("construct kube config: %w", err) + } + + clientFactory, err := kube.NewClientFactory(ctx, kubeConfig) + if err != nil { + return nil, fmt.Errorf("construct kube client factory: %w", err) + } + + releaseStorage, err := release.NewReleaseStorage(ctx, releaseNamespace, opts.ReleaseStorageDriver, clientFactory, release.ReleaseStorageOptions{ + SQLConnection: opts.ReleaseStorageSQLConnection, + }) + if err != nil { + return nil, fmt.Errorf("construct release storage: %w", err) + } + + loader.NoChartLockWarning = "" + + log.Default.Info(ctx, "Build release history") + + history, err := release.BuildHistory(releaseName, releaseStorage, release.HistoryOptions{}) + if err != nil { + return nil, fmt.Errorf("build release history: %w", err) + } + + result := &ReleaseHistoryResultV1{ + APIVersion: "v1", + } + + releases := history.Releases() + if len(releases) == 0 { + return nil, &ReleaseNotFoundError{ + ReleaseName: releaseName, + ReleaseNamespace: releaseNamespace, + } + } + + for _, release := range releases { + result.Releases = append(result.Releases, &ReleaseHistoryResultRelease{ + Annotations: release.Info.Annotations, + Chart: &ReleaseHistoryResultChart{ + Name: release.Chart.Name(), + Version: release.Chart.Metadata.Version, + AppVersion: release.Chart.Metadata.AppVersion, + }, + DeployedAt: &ReleaseHistoryResultDeployedAt{ + Human: release.Info.LastDeployed.String(), + Unix: int(release.Info.LastDeployed.Unix()), + }, + Name: release.Name, + Namespace: release.Namespace, + Revision: release.Version, + Status: release.Info.Status, + }) + } + + sort.SliceStable(result.Releases, func(i, j int) bool { + return result.Releases[i].Revision < result.Releases[j].Revision + }) + + if opts.RevisionsLimit > 0 && len(result.Releases) > opts.RevisionsLimit { + result.Releases = result.Releases[len(result.Releases)-opts.RevisionsLimit:] + } + + if opts.OutputNoPrint { + return result, nil + } + + var resultMessage string + + switch opts.OutputFormat { + case common.OutputFormatTable: + table := buildReleaseHistoryOutputTable(ctx, result) + resultMessage = table.Render() + "\n" + case common.OutputFormatJSON: + b, err := json.MarshalIndent(result, "", strings.Repeat(" ", 2)) + if err != nil { + return nil, fmt.Errorf("marshal result to json: %w", err) + } + + resultMessage = string(b) + "\n" + case common.OutputFormatYAML: + b, err := yaml.MarshalContext(ctx, result, yaml.UseLiteralStyleIfMultiline(true)) + if err != nil { + return nil, fmt.Errorf("marshal result to yaml: %w", err) + } + + resultMessage = string(b) + default: + return nil, fmt.Errorf("unknown output format %q", opts.OutputFormat) + } + + var colorLevel color.Level + if color.Enable { + colorLevel = color.TermColorLevel() + } + + if err := writeWithSyntaxHighlight(os.Stdout, resultMessage, opts.OutputFormat, colorLevel); err != nil { + return nil, fmt.Errorf("write result to output: %w", err) + } + + return result, nil +} + +func buildReleaseHistoryOutputTable(ctx context.Context, result *ReleaseHistoryResultV1) prtable.Writer { + table := prtable.NewWriter() + setReleaseHistoryOutputTableStyle(ctx, table) + + headerRow := prtable.Row{ + color.New(color.Bold).Sprintf("REVISION"), + color.New(color.Bold).Sprintf("STATUS"), + color.New(color.Bold).Sprintf("DEPLOYED"), + color.New(color.Bold).Sprintf("CHART"), + color.New(color.Bold).Sprintf("CHART VERSION"), + color.New(color.Bold).Sprintf("APP VERSION"), + } + + table.AppendHeader(headerRow) + + for _, release := range result.Releases { + var statusColor color.Color + switch release.Status { + case helmreleasestatus.StatusDeployed, helmreleasestatus.StatusSuperseded: + statusColor = color.Green + case helmreleasestatus.StatusFailed: + statusColor = color.LightRed + default: + statusColor = color.LightYellow + } + + row := prtable.Row{ + release.Revision, + color.New(statusColor).Sprint(release.Status), + time.Unix(int64(release.DeployedAt.Unix), 0).Format(time.RFC822), + release.Chart.Name, + release.Chart.Version, + release.Chart.AppVersion, + } + + table.AppendRow(row) + } + + return table +} + +func applyReleaseHistoryOptionsDefaults(opts ReleaseHistoryOptions, homeDir string) (ReleaseHistoryOptions, error) { + var err error + if opts.TempDirPath == "" { + opts.TempDirPath, err = os.MkdirTemp("", "") + if err != nil { + return ReleaseHistoryOptions{}, fmt.Errorf("create temp dir: %w", err) + } + } + + opts.KubeConnectionOptions.ApplyDefaults(homeDir) + + if opts.ReleaseStorageDriver == common.ReleaseStorageDriverDefault { + opts.ReleaseStorageDriver = common.ReleaseStorageDriverSecrets + } + + if opts.OutputFormat == "" { + opts.OutputFormat = DefaultReleaseHistoryOutputFormat + } + + return opts, nil +} + +func setReleaseHistoryOutputTableStyle(ctx context.Context, table prtable.Writer) { + style := prtable.StyleBoxDefault + style.PaddingLeft = "" + style.PaddingRight = " " + + columnConfigs := []prtable.ColumnConfig{ + { + Number: 1, + Align: text.AlignLeft, + }, + { + Number: 2, + Align: text.AlignLeft, + }, + { + Number: 3, + Align: text.AlignLeft, + }, + { + Number: 4, + Align: text.AlignLeft, + }, + { + Number: 5, + Align: text.AlignLeft, + }, + { + Number: 6, + Align: text.AlignLeft, + }, + } + + tableWidth := log.Default.BlockContentWidth(ctx) + if tableWidth < 20 { + tableWidth = 140 + } else if tableWidth > 200 { + tableWidth = 200 + } + + paddingsWidth := len(columnConfigs) * (len(style.PaddingLeft) + len(style.PaddingRight)) + + columnConfigs[0].WidthMax = 10 + columnConfigs[1].WidthMax = 16 + columnConfigs[2].WidthMax = 30 + columnConfigs[4].WidthMax = 16 + columnConfigs[5].WidthMax = 16 + + fixedWidth := columnConfigs[0].WidthMax + columnConfigs[1].WidthMax + columnConfigs[2].WidthMax + columnConfigs[4].WidthMax + columnConfigs[5].WidthMax + columnConfigs[3].WidthMax = tableWidth - paddingsWidth - fixedWidth + + table.SetColumnConfigs(columnConfigs) + table.SetStyle(prtable.Style{ + Box: style, + Color: prtable.ColorOptionsDefault, + Format: prtable.FormatOptionsDefault, + HTML: prtable.DefaultHTMLOptions, + Options: prtable.OptionsNoBordersAndSeparators, + Title: prtable.TitleOptionsDefault, + }) + table.SuppressTrailingSpaces() +} diff --git a/pkg/action/release_install.go b/pkg/action/release_install.go index 4f56091d..6de3977e 100644 --- a/pkg/action/release_install.go +++ b/pkg/action/release_install.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "sort" "time" @@ -15,15 +14,15 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/werf/kubedog/pkg/dyntracker/logstore" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/kubedog/pkg/informer" - "github.com/werf/kubedog/pkg/trackers/dyntracker/logstore" - "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" "github.com/werf/nelm/pkg/chart" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/helm/pkg/registry" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/legacy/progrep" "github.com/werf/nelm/pkg/lock" @@ -56,8 +55,6 @@ type ReleaseInstallOptions struct { // ChartAppVersion overrides the appVersion field in Chart.yaml. // Used to set application version metadata without modifying the chart file. ChartAppVersion string - // ChartDirPath is deprecated - ChartDirPath string // TODO(major): get rid // ChartProvenanceKeyring is the path to a keyring file containing public keys // used to verify chart provenance signatures. Used with signed charts for security. ChartProvenanceKeyring string @@ -88,13 +85,15 @@ type ReleaseInstallOptions struct { InstallReportPath string // LegacyChartType specifies the chart type for legacy compatibility. // Used internally for backward compatibility with werf integration. - LegacyChartType helmopts.ChartType + LegacyChartType common.LegacyChartType // LegacyExtraValues provides additional values programmatically. // Used internally for backward compatibility with werf integration. LegacyExtraValues map[string]interface{} // LegacyLogRegistryStreamOut is the output writer for Helm registry client logs. // Defaults to io.Discard if not set. Used for debugging registry operations. LegacyLogRegistryStreamOut io.Writer + // LegacyPlanArtifact provides plan artifact as a result of the release plan install action. + LegacyPlanArtifact *PlanArtifact // LegacyProgressReportCh, when non-nil, receives ProgressReport snapshots during deployment. // Must be a buffered channel with capacity >= 1. The caller owns the channel and is responsible // for its lifecycle. Intermediate reports may be dropped if the consumer is slow; the final @@ -172,8 +171,6 @@ func ReleaseInstall(ctx context.Context, releaseName, releaseNamespace string, o } func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, releaseName, releaseNamespace string, opts ReleaseInstallOptions) error { - usePlan := opts.PlanArtifactPath != "" - currentDir, err := os.Getwd() if err != nil { return fmt.Errorf("get current working directory: %w", err) @@ -193,20 +190,27 @@ func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, re lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) } - var planArtifact *plan.PlanArtifact - if usePlan { + var planArtifact *PlanArtifact + + if opts.PlanArtifactPath != "" { log.Default.Info(ctx, "Using %s plan artifact", opts.PlanArtifactPath) log.Default.Debug(ctx, "Read plan artifact") - planArtifact, err = plan.ReadPlanArtifact(ctx, opts.PlanArtifactPath, opts.SecretKey, opts.SecretWorkDir) + planArtifact, err = ReadPlanArtifact(ctx, opts.PlanArtifactPath, opts.SecretKey, opts.SecretWorkDir) if err != nil { return fmt.Errorf("read plan artifact from %s: %w", opts.PlanArtifactPath, err) } + } else { + planArtifact = opts.LegacyPlanArtifact + } + + usePlan := planArtifact != nil + if usePlan { log.Default.Debug(ctx, "Validate plan artifact") - if err := plan.ValidatePlanArtifact(planArtifact, opts.PlanArtifactLifetime); err != nil { + if err := ValidatePlanArtifact(planArtifact, opts.PlanArtifactLifetime); err != nil { return fmt.Errorf("validate plan artifact: %w", err) } @@ -216,16 +220,7 @@ func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, re opts.ReleaseInstallRuntimeOptions = planArtifact.Data.Options } - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: releaseNamespace, // TODO: unset it everywhere }) @@ -323,7 +318,7 @@ func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, re instResInfos = planArtifact.Data.InstallableResourceInfos relInfos = planArtifact.Data.ReleaseInfos } else { - prevReleaseFailed := prevRelease != nil && prevRelease.IsStatusFailed() + prevReleaseFailed := prevRelease != nil && prevRelease.Info.Status == helmreleasestatus.StatusFailed var deployType common.DeployType if prevDeployedRelease != nil { @@ -334,8 +329,8 @@ func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, re deployType = common.DeployTypeInitial } - helmOptions := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ + helmOptions := common.HelmOptions{ + ChartLoadOpts: common.ChartLoadOptions{ ChartAppVersion: opts.ChartAppVersion, ChartType: opts.LegacyChartType, DefaultChartAPIVersion: opts.DefaultChartAPIVersion, @@ -513,12 +508,12 @@ func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, re if releaseIsUpToDate && installPlanIsUseless { if opts.InstallReportPath != "" { - if err := saveReport(opts.InstallReportPath, &releaseReportV3{ - Version: 3, - Release: releaseName, - Namespace: releaseNamespace, - Revision: newRelease.Version, - Status: helmrelease.StatusSkipped, + if err := saveReport(opts.InstallReportPath, &ReleaseReportV3{ + APIVersion: "v3", + Release: releaseName, + Namespace: releaseNamespace, + Revision: newRelease.Version, + Status: helmreleasestatus.Status("skipped"), }); err != nil { return fmt.Errorf("save release install report: %w", err) } @@ -657,12 +652,12 @@ func releaseInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, re sort.Strings(reportCanceledOps) sort.Strings(reportFailedOps) - report := &releaseReportV3{ - Version: 3, + report := &ReleaseReportV3{ + APIVersion: "v3", Release: releaseName, Namespace: releaseNamespace, Revision: newRelease.Version, - Status: lo.Ternary(executePlanErr == nil, helmrelease.StatusDeployed, helmrelease.StatusFailed), + Status: lo.Ternary(executePlanErr == nil, helmreleasestatus.StatusDeployed, helmreleasestatus.StatusFailed), CompletedOperations: reportCompletedOps, CanceledOperations: reportCanceledOps, FailedOperations: reportFailedOps, @@ -709,9 +704,7 @@ func applyReleaseInstallOptionsDefaults(opts ReleaseInstallOptions, currentDir, opts.SecretValuesOptions.ApplyDefaults(currentDir) opts.TrackingOptions.ApplyDefaults() - if opts.Chart == "" && opts.ChartDirPath != "" { - opts.Chart = opts.ChartDirPath - } else if opts.ChartDirPath == "" && opts.Chart == "" { + if opts.Chart == "" { opts.Chart = currentDir } diff --git a/pkg/action/release_list.go b/pkg/action/release_list.go index a0a75db4..26ef9e82 100644 --- a/pkg/action/release_list.go +++ b/pkg/action/release_list.go @@ -5,10 +5,8 @@ import ( "encoding/json" "fmt" "os" - "path/filepath" "sort" "strings" - "time" "github.com/goccy/go-yaml" "github.com/gookit/color" @@ -18,7 +16,7 @@ import ( "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/release" @@ -66,7 +64,7 @@ type ReleaseListResultRelease struct { Name string `json:"name"` Namespace string `json:"namespace"` Revision int `json:"revision"` - Status helmrelease.Status `json:"status"` + Status helmreleasestatus.Status `json:"status"` DeployedAt *ReleaseListResultDeployedAt `json:"deployedAt"` Annotations map[string]string `json:"annotations"` Chart *ReleaseListResultChart `json:"chart"` @@ -95,16 +93,7 @@ func ReleaseList(ctx context.Context, opts ReleaseListOptions) (*ReleaseListResu return nil, fmt.Errorf("build release list options: %w", err) } - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: opts.ReleaseNamespace, // TODO: unset it everywhere }) @@ -149,8 +138,8 @@ func ReleaseList(ctx context.Context, opts ReleaseListOptions) (*ReleaseListResu AppVersion: lastRelease.Chart.Metadata.AppVersion, }, DeployedAt: &ReleaseListResultDeployedAt{ - Human: time.Time{}.String(), - Unix: int(time.Time{}.Unix()), + Human: lastRelease.Info.LastDeployed.String(), + Unix: int(lastRelease.Info.LastDeployed.Unix()), }, Name: lastRelease.Name, Namespace: lastRelease.Namespace, @@ -225,9 +214,9 @@ func buildReleaseListOutputTable(ctx context.Context, result *ReleaseListResultV for _, release := range result.Releases { var statusColor color.Color switch release.Status { - case helmrelease.StatusDeployed, helmrelease.StatusSuperseded: + case helmreleasestatus.StatusDeployed, helmreleasestatus.StatusSuperseded: statusColor = color.Green - case helmrelease.StatusFailed: + case helmreleasestatus.StatusFailed: statusColor = color.LightRed default: statusColor = color.LightYellow diff --git a/pkg/action/release_plan_install.go b/pkg/action/release_plan_install.go index 7ef4c83f..ba09cc70 100644 --- a/pkg/action/release_plan_install.go +++ b/pkg/action/release_plan_install.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "strings" "time" @@ -16,9 +15,8 @@ import ( "github.com/werf/nelm/pkg/chart" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/plan" @@ -51,9 +49,6 @@ type ReleasePlanInstallOptions struct { // ChartAppVersion overrides the appVersion field in Chart.yaml. // Used to set application version metadata without modifying the chart file. ChartAppVersion string - // ChartDirPath is deprecated - // TODO(major): get rid - ChartDirPath string // ChartProvenanceKeyring is the path to a keyring file containing public keys // used to verify chart provenance signatures. Used with signed charts for security. ChartProvenanceKeyring string @@ -84,7 +79,7 @@ type ReleasePlanInstallOptions struct { InstallGraphPath string // LegacyChartType specifies the chart type for legacy compatibility. // Used internally for backward compatibility with werf integration. - LegacyChartType helmopts.ChartType + LegacyChartType common.LegacyChartType // LegacyExtraValues provides additional values programmatically. // Used internally for backward compatibility with werf integration. LegacyExtraValues map[string]interface{} @@ -117,7 +112,7 @@ type ReleasePlanInstallOptions struct { } // Plans the next release installation without applying changes to the cluster. -func ReleasePlanInstall(ctx context.Context, releaseName, releaseNamespace string, opts ReleasePlanInstallOptions) error { +func ReleasePlanInstall(ctx context.Context, releaseName, releaseNamespace string, opts ReleasePlanInstallOptions) (*PlanArtifact, error) { ctx, ctxCancelFn := context.WithCancelCause(ctx) if opts.Timeout == 0 { @@ -127,61 +122,58 @@ func ReleasePlanInstall(ctx context.Context, releaseName, releaseNamespace strin ctx, _ = context.WithTimeoutCause(ctx, opts.Timeout, fmt.Errorf("context timed out: action timed out after %s", opts.Timeout.String())) defer ctxCancelFn(fmt.Errorf("context canceled: action finished")) - actionCh := make(chan error, 1) + type actionResult struct { + artifact *PlanArtifact // Replace with your actual type + err error + } + + actionCh := make(chan actionResult, 1) go func() { - actionCh <- releasePlanInstall(ctx, ctxCancelFn, releaseName, releaseNamespace, opts) + planArtifact, err := releasePlanInstall(ctx, ctxCancelFn, releaseName, releaseNamespace, opts) + actionCh <- actionResult{artifact: planArtifact, err: err} }() for { select { - case err := <-actionCh: - return err + case res := <-actionCh: + return res.artifact, res.err case <-ctx.Done(): - return context.Cause(ctx) + return nil, context.Cause(ctx) } } } -func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, releaseName, releaseNamespace string, opts ReleasePlanInstallOptions) error { +func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, releaseName, releaseNamespace string, opts ReleasePlanInstallOptions) (*PlanArtifact, error) { currentDir, err := os.Getwd() if err != nil { - return fmt.Errorf("get current working directory: %w", err) + return nil, fmt.Errorf("get current working directory: %w", err) } homeDir, err := os.UserHomeDir() if err != nil { - return fmt.Errorf("get home directory: %w", err) + return nil, fmt.Errorf("get home directory: %w", err) } opts, err = applyReleasePlanInstallOptionsDefaults(opts, currentDir, homeDir) if err != nil { - return fmt.Errorf("build release plan install options: %w", err) + return nil, fmt.Errorf("build release plan install options: %w", err) } if opts.SecretKey != "" { lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) } - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: releaseNamespace, // TODO: unset it everywhere }) if err != nil { - return fmt.Errorf("construct kube config: %w", err) + return nil, fmt.Errorf("construct kube config: %w", err) } clientFactory, err := kube.NewClientFactory(ctx, kubeConfig) if err != nil { - return fmt.Errorf("construct kube client factory: %w", err) + return nil, fmt.Errorf("construct kube client factory: %w", err) } helmRegistryClientOpts := []registry.ClientOption{ @@ -199,18 +191,18 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc helmRegistryClient, err := registry.NewClient(helmRegistryClientOpts...) if err != nil { - return fmt.Errorf("construct registry client: %w", err) + return nil, fmt.Errorf("construct registry client: %w", err) } releaseStorage, err := release.NewReleaseStorage(ctx, releaseNamespace, opts.ReleaseStorageDriver, clientFactory, release.ReleaseStorageOptions{ SQLConnection: opts.ReleaseStorageSQLConnection, }) if err != nil { - return fmt.Errorf("construct release storage: %w", err) + return nil, fmt.Errorf("construct release storage: %w", err) } - helmOptions := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ + helmOptions := common.HelmOptions{ + ChartLoadOpts: common.ChartLoadOptions{ ChartAppVersion: opts.ChartAppVersion, ChartType: opts.LegacyChartType, DefaultChartAPIVersion: opts.DefaultChartAPIVersion, @@ -231,7 +223,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc history, err := release.BuildHistory(releaseName, releaseStorage, release.HistoryOptions{}) if err != nil { - return fmt.Errorf("build release history: %w", err) + return nil, fmt.Errorf("build release history: %w", err) } releases := history.Releases() @@ -246,7 +238,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc if prevRelease != nil { newRevision = prevRelease.Version + 1 - prevReleaseFailed = prevRelease.IsStatusFailed() + prevReleaseFailed = prevRelease.Info.Status == helmreleasestatus.StatusFailed } else { newRevision = 1 } @@ -278,7 +270,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc DenoBinaryPath: opts.DenoBinaryPath, }) if err != nil { - return fmt.Errorf("render chart: %w", err) + return nil, fmt.Errorf("render chart: %w", err) } log.Default.Debug(ctx, "Build transformed resource specs") @@ -288,7 +280,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc spec.NewDropInvalidAnnotationsAndLabelsTransformer(), }) if err != nil { - return fmt.Errorf("build transformed resource specs: %w", err) + return nil, fmt.Errorf("build transformed resource specs: %w", err) } log.Default.Debug(ctx, "Build releasable resource specs") @@ -304,7 +296,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc releasableResSpecs, err := spec.BuildReleasableResourceSpecs(ctx, releaseNamespace, transformedResSpecs, patchers) if err != nil { - return fmt.Errorf("build releasable resource specs: %w", err) + return nil, fmt.Errorf("build releasable resource specs: %w", err) } newRelease, err := release.NewRelease(releaseName, releaseNamespace, newRevision, deployType, releasableResSpecs, renderChartResult.Chart, renderChartResult.ReleaseConfig, release.ReleaseOptions{ @@ -313,7 +305,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc Notes: renderChartResult.Notes, }) if err != nil { - return fmt.Errorf("construct new release: %w", err) + return nil, fmt.Errorf("construct new release: %w", err) } log.Default.Debug(ctx, "Convert previous release to resource specs") @@ -322,7 +314,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc if prevRelease != nil { prevRelResSpecs, err = release.ReleaseToResourceSpecs(prevRelease, releaseNamespace, false) if err != nil { - return fmt.Errorf("convert previous release to resource specs: %w", err) + return nil, fmt.Errorf("convert previous release to resource specs: %w", err) } } @@ -330,7 +322,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc newRelResSpecs, err := release.ReleaseToResourceSpecs(newRelease, releaseNamespace, false) if err != nil { - return fmt.Errorf("convert new release to resource specs: %w", err) + return nil, fmt.Errorf("convert new release to resource specs: %w", err) } log.Default.Debug(ctx, "Build resources") @@ -343,13 +335,13 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc DefaultDeletePropagation: metav1.DeletionPropagation(opts.DefaultDeletePropagation), }) if err != nil { - return fmt.Errorf("build resources: %w", err) + return nil, fmt.Errorf("build resources: %w", err) } log.Default.Debug(ctx, "Locally validate resources") if err := resource.ValidateLocal(ctx, releaseNamespace, instResources, opts.ResourceValidationOptions); err != nil { - return fmt.Errorf("locally validate resources: %w", err) + return nil, fmt.Errorf("locally validate resources: %w", err) } log.Default.Debug(ctx, "Build resource infos") @@ -360,7 +352,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc if lastDeployedOrLastRelease != nil { lastDeployedOrLastRelResSpecs, err = release.ReleaseToResourceSpecs(lastDeployedOrLastRelease, releaseNamespace, false) if err != nil { - return fmt.Errorf("convert last deployed or last release to resource specs: %w", err) + return nil, fmt.Errorf("convert last deployed or last release to resource specs: %w", err) } } @@ -370,20 +362,20 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc LastDeployedOrLastRelResourceSpecs: lastDeployedOrLastRelResSpecs, }) if err != nil { - return fmt.Errorf("build resource infos: %w", err) + return nil, fmt.Errorf("build resource infos: %w", err) } log.Default.Debug(ctx, "Remotely validate resources") if err := plan.ValidateRemote(releaseName, releaseNamespace, instResInfos, opts.ForceAdoption); err != nil { - return fmt.Errorf("remotely validate resources: %w", err) + return nil, fmt.Errorf("remotely validate resources: %w", err) } log.Default.Debug(ctx, "Build release infos") relInfos, err := plan.BuildReleaseInfos(ctx, deployType, releases, newRelease) if err != nil { - return fmt.Errorf("build release infos: %w", err) + return nil, fmt.Errorf("build release infos: %w", err) } log.Default.Debug(ctx, "Build install plan") @@ -394,18 +386,18 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc if err != nil { handleBuildPlanErr(ctx, installPlan, err, opts.InstallGraphPath, opts.TempDirPath, "release-install-graph.dot") - return fmt.Errorf("build install plan: %w", err) + return nil, fmt.Errorf("build install plan: %w", err) } if opts.InstallGraphPath != "" { if err := savePlanAsDot(installPlan, opts.InstallGraphPath); err != nil { - return fmt.Errorf("save release install graph: %w", err) + return nil, fmt.Errorf("save release install graph: %w", err) } } releaseIsUpToDate, err := release.IsReleaseUpToDate(prevRelease, newRelease) if err != nil { - return fmt.Errorf("check if release is up to date: %w", err) + return nil, fmt.Errorf("check if release is up to date: %w", err) } installPlanIsUseless := lo.NoneBy(installPlan.Operations(), func(op *plan.Operation) bool { @@ -421,7 +413,7 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc changes, err := plan.CalculatePlannedChanges(instResInfos, delResInfos) if err != nil { - return fmt.Errorf("calculate planned changes: %w", err) + return nil, fmt.Errorf("calculate planned changes: %w", err) } if releaseIsUpToDate && installPlanIsUseless { @@ -430,62 +422,56 @@ func releasePlanInstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc log.Default.Info(ctx, color.Style{color.Bold, color.Yellow}.Render(fmt.Sprintf("No resource changes planned, but still must install release %q (namespace: %q)", releaseName, releaseNamespace))) } - if err := logPlannedChanges(ctx, releaseName, releaseNamespace, changes, opts.ResourceDiffOptions); err != nil { - return fmt.Errorf("log planned changes: %w", err) + planArtifact := &PlanArtifact{ + APIVersion: PlanArtifactSchemeVersion, + Data: &PlanArtifactData{ + Options: opts.ReleaseInstallRuntimeOptions, + Release: newRelease, + Plan: installPlan, + Changes: changes, + InstallableResourceInfos: instResInfos, + ReleaseInfos: relInfos, + }, + DeployType: deployType, + Release: PlanArtifactRelease{ + Name: releaseName, + Namespace: releaseNamespace, + Revision: newRelease.Version, + }, + Timestamp: time.Now().UTC(), + } + + if err := logPlannedChanges(ctx, planArtifact, opts.ResourceDiffOptions); err != nil { + return nil, fmt.Errorf("log planned changes: %w", err) } if opts.PlanArtifactPath != "" { - planArtifact := &plan.PlanArtifact{ - APIVersion: plan.PlanArtifactSchemeVersion, - Data: &plan.PlanArtifactData{ - Options: opts.ReleaseInstallRuntimeOptions, - Release: newRelease, - Plan: installPlan, - Changes: changes, - InstallableResourceInfos: instResInfos, - ReleaseInfos: relInfos, - }, - DeployType: deployType, - Release: plan.PlanArtifactRelease{ - Name: releaseName, - Namespace: releaseNamespace, - Revision: newRelease.Version, - }, - Timestamp: time.Now().UTC(), - } - - if err := plan.WritePlanArtifact(ctx, planArtifact, opts.PlanArtifactPath, opts.SecretKey, opts.SecretWorkDir); err != nil { - return fmt.Errorf("save install plan to %q: %w", opts.PlanArtifactPath, err) + if err := WritePlanArtifact(ctx, planArtifact, opts.PlanArtifactPath, opts.SecretKey, opts.SecretWorkDir); err != nil { + return nil, fmt.Errorf("save install plan to %q: %w", opts.PlanArtifactPath, err) } } if opts.ErrorIfChangesPlanned { - if featgate.FeatGateMoreDetailedExitCodeForPlan.Enabled() || featgate.FeatGatePreviewV2.Enabled() { - if releaseIsUpToDate && installPlanIsUseless { - return nil - } else if installPlanIsUseless || len(changes) == 0 { - return ErrReleaseInstallPlanned - } else { - return ErrResourceChangesPlanned - } - } else { - if !releaseIsUpToDate || !installPlanIsUseless { - return ErrChangesPlanned - } + if releaseIsUpToDate && installPlanIsUseless { + return planArtifact, nil + } else if installPlanIsUseless || len(changes) == 0 { + return planArtifact, ErrReleaseInstallPlanned } + + return planArtifact, ErrResourceChangesPlanned } - return nil + return planArtifact, nil } -func logPlannedChanges(ctx context.Context, releaseName, releaseNamespace string, changes []*plan.ResourceChange, opts common.ResourceDiffOptions) error { - if len(changes) == 0 { +func logPlannedChanges(ctx context.Context, planArtifact *PlanArtifact, opts common.ResourceDiffOptions) error { + if len(planArtifact.Data.Changes) == 0 { return nil } log.Default.Info(ctx, "") - for _, change := range changes { + for _, change := range planArtifact.Data.Changes { if err := log.Default.InfoBlockErr(ctx, log.BlockOptions{ BlockTitle: buildDiffHeader(change), }, func() error { @@ -506,10 +492,10 @@ func logPlannedChanges(ctx context.Context, releaseName, releaseNamespace string } } - log.Default.Info(ctx, color.Bold.Render("Planned changes summary")+" for release %q (namespace: %q):", releaseName, releaseNamespace) + log.Default.Info(ctx, color.Bold.Render("Planned changes summary")+" for release %q (namespace: %q):", planArtifact.Release.Name, planArtifact.Release.Namespace) for _, changeType := range []string{"create", "recreate", "update", "blind apply", "delete"} { - logSummaryLine(ctx, changes, changeType) + logSummaryLine(ctx, planArtifact.Data.Changes, changeType) } log.Default.Info(ctx, "") @@ -532,9 +518,7 @@ func applyReleasePlanInstallOptionsDefaults(opts ReleasePlanInstallOptions, curr opts.ResourceDiffOptions.ApplyDefaults() opts.SecretValuesOptions.ApplyDefaults(currentDir) - if opts.Chart == "" && opts.ChartDirPath != "" { - opts.Chart = opts.ChartDirPath - } else if opts.ChartDirPath == "" && opts.Chart == "" { + if opts.Chart == "" { opts.Chart = currentDir } diff --git a/pkg/action/release_plan_show.go b/pkg/action/release_plan_show.go index 47ed3590..b68bf94f 100644 --- a/pkg/action/release_plan_show.go +++ b/pkg/action/release_plan_show.go @@ -9,7 +9,6 @@ import ( "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/log" - "github.com/werf/nelm/pkg/plan" ) const DefaultReleasePlanShowLogLevel = log.InfoLevel @@ -17,7 +16,9 @@ const DefaultReleasePlanShowLogLevel = log.InfoLevel type ReleasePlanShowOptions struct { common.ResourceDiffOptions - // PlanArtifactPath is the path to the plan artifact file to execute. + // LegacyPlanArtifact provides plan artifact to review changes. + LegacyPlanArtifact *PlanArtifact + // PlanArtifactPath is the path to the plan artifact file to review changes. PlanArtifactPath string // SecretKey is the encryption/decryption key for the plan artifact file. SecretKey string @@ -42,14 +43,22 @@ func ReleasePlanShow(ctx context.Context, opts ReleasePlanShowOptions) error { lo.Must0(os.Setenv("WERF_SECRET_KEY", opts.SecretKey)) } - log.Default.Debug(ctx, "Read plan artifact") + var planArtifact *PlanArtifact - planArtifact, err := plan.ReadPlanArtifact(ctx, opts.PlanArtifactPath, opts.SecretKey, opts.SecretWorkDir) - if err != nil { - return fmt.Errorf("read plan artifact from %s: %w", opts.PlanArtifactPath, err) + if opts.LegacyPlanArtifact != nil { + planArtifact = opts.LegacyPlanArtifact + } else { + var err error + + log.Default.Debug(ctx, "Read plan artifact") + + planArtifact, err = ReadPlanArtifact(ctx, opts.PlanArtifactPath, opts.SecretKey, opts.SecretWorkDir) + if err != nil { + return fmt.Errorf("read plan artifact from %s: %w", opts.PlanArtifactPath, err) + } } - if err := logPlannedChanges(ctx, planArtifact.Release.Name, planArtifact.Release.Namespace, planArtifact.Data.Changes, opts.ResourceDiffOptions); err != nil { + if err := logPlannedChanges(ctx, planArtifact, opts.ResourceDiffOptions); err != nil { return fmt.Errorf("log planned changes: %w", err) } diff --git a/pkg/action/release_rollback.go b/pkg/action/release_rollback.go index 183a7823..08884a15 100644 --- a/pkg/action/release_rollback.go +++ b/pkg/action/release_rollback.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" "sort" "time" @@ -12,12 +11,13 @@ import ( "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "github.com/werf/kubedog/pkg/dyntracker/logstore" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/kubedog/pkg/informer" - "github.com/werf/kubedog/pkg/trackers/dyntracker/logstore" - "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/lock" "github.com/werf/nelm/pkg/log" @@ -127,16 +127,7 @@ func releaseRollback(ctx context.Context, ctxCancelFn context.CancelCauseFunc, r return fmt.Errorf("build release rollback options: %w", err) } - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: releaseNamespace, // TODO: unset it everywhere }) @@ -223,7 +214,7 @@ func releaseRollback(ctx context.Context, ctxCancelFn context.CancelCauseFunc, r if prevRelease != nil { newRevision = prevRelease.Version + 1 - prevReleaseFailed = prevRelease.IsStatusFailed() + prevReleaseFailed = prevRelease.Info.Status == helmreleasestatus.StatusFailed } else { newRevision = 1 } @@ -238,7 +229,7 @@ func releaseRollback(ctx context.Context, ctxCancelFn context.CancelCauseFunc, r } newRelease, err := release.NewRelease(releaseName, releaseNamespace, newRevision, deployType, rollbackReleaseResSpecs, rollbackRelease.Chart, rollbackRelease.Config, release.ReleaseOptions{ - InfoAnnotations: lo.Assign(rollbackRelease.Info.Annotations, opts.ReleaseInfoAnnotations), + InfoAnnotations: opts.ReleaseInfoAnnotations, Labels: lo.Assign(rollbackRelease.Labels, opts.ReleaseLabels), Notes: rollbackRelease.Info.Notes, }) @@ -352,12 +343,12 @@ func releaseRollback(ctx context.Context, ctxCancelFn context.CancelCauseFunc, r if releaseIsUpToDate && installPlanIsUseless { if opts.RollbackReportPath != "" { - if err := saveReport(opts.RollbackReportPath, &releaseReportV3{ - Version: 3, - Release: releaseName, - Namespace: releaseNamespace, - Revision: newRelease.Version, - Status: helmrelease.StatusSkipped, + if err := saveReport(opts.RollbackReportPath, &ReleaseReportV3{ + APIVersion: "v3", + Release: releaseName, + Namespace: releaseNamespace, + Revision: newRelease.Version, + Status: helmreleasestatus.Status("skipped"), }); err != nil { return fmt.Errorf("save release install report: %w", err) } @@ -466,12 +457,12 @@ func releaseRollback(ctx context.Context, ctxCancelFn context.CancelCauseFunc, r sort.Strings(reportCanceledOps) sort.Strings(reportFailedOps) - report := &releaseReportV3{ - Version: 3, + report := &ReleaseReportV3{ + APIVersion: "v3", Release: releaseName, Namespace: releaseNamespace, Revision: newRelease.Version, - Status: helmrelease.StatusDeployed, + Status: helmreleasestatus.StatusDeployed, CompletedOperations: reportCompletedOps, CanceledOperations: reportCanceledOps, FailedOperations: reportFailedOps, diff --git a/pkg/action/release_uninstall.go b/pkg/action/release_uninstall.go index f2dd37d3..c0b71fd2 100644 --- a/pkg/action/release_uninstall.go +++ b/pkg/action/release_uninstall.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "os" - "path/filepath" "sort" "time" @@ -13,13 +12,13 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "github.com/werf/kubedog/pkg/dyntracker" + "github.com/werf/kubedog/pkg/dyntracker/logstore" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/kubedog/pkg/informer" - "github.com/werf/kubedog/pkg/trackers/dyntracker" - "github.com/werf/kubedog/pkg/trackers/dyntracker/logstore" - "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/legacy/progrep" "github.com/werf/nelm/pkg/lock" @@ -121,16 +120,7 @@ func releaseUninstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, return fmt.Errorf("build release uninstall options: %w", err) } - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ + kubeConfig, err := kube.NewKubeConfig(ctx, kube.KubeConfigOptions{ KubeConnectionOptions: opts.KubeConnectionOptions, KubeContextNamespace: releaseNamespace, // TODO: unset it everywhere }) @@ -209,7 +199,7 @@ func releaseUninstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, } prevRelease := lo.LastOrEmpty(releases) - prevReleaseFailed := prevRelease.IsStatusFailed() + prevReleaseFailed := prevRelease.Info.Status == helmreleasestatus.StatusFailed deployType := common.DeployTypeUninstall log.Default.Debug(ctx, "Convert previous release to resource specs") @@ -354,12 +344,12 @@ func releaseUninstall(ctx context.Context, ctxCancelFn context.CancelCauseFunc, sort.Strings(reportCanceledOps) sort.Strings(reportFailedOps) - report := &releaseReportV3{ - Version: 3, + report := &ReleaseReportV3{ + APIVersion: "v3", Release: releaseName, Namespace: releaseNamespace, Revision: prevRelease.Version, - Status: helmrelease.StatusUninstalled, + Status: helmreleasestatus.StatusUninstalled, CompletedOperations: reportCompletedOps, CanceledOperations: reportCanceledOps, FailedOperations: reportFailedOps, diff --git a/pkg/action/release_uninstall_legacy.go b/pkg/action/release_uninstall_legacy.go deleted file mode 100644 index 653a2bde..00000000 --- a/pkg/action/release_uninstall_legacy.go +++ /dev/null @@ -1,358 +0,0 @@ -package action - -import ( - "context" - "errors" - "flag" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sync" - "time" - - "github.com/gookit/color" - "github.com/samber/lo" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/klog" - klogv2 "k8s.io/klog/v2" - - "github.com/werf/kubedog/pkg/display" - kdkube "github.com/werf/kubedog/pkg/kube" - "github.com/werf/logboek" - "github.com/werf/nelm/pkg/common" - helmv3 "github.com/werf/nelm/pkg/helm/cmd/helm" - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/cli" - helmkube "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - "github.com/werf/nelm/pkg/kube" - "github.com/werf/nelm/pkg/legacy/deploy" - "github.com/werf/nelm/pkg/lock" - "github.com/werf/nelm/pkg/log" - "github.com/werf/nelm/pkg/resource/spec" -) - -const DefaultLegacyReleaseUninstallLogLevel = log.InfoLevel - -var legacyUninstallLock sync.Mutex - -type LegacyReleaseUninstallOptions struct { - common.KubeConnectionOptions - common.TrackingOptions - - DeleteReleaseNamespace bool - NetworkParallelism int - NoDeleteHooks bool - ReleaseHistoryLimit int - ReleaseStorageDriver string - TempDirPath string - Timeout time.Duration -} - -func LegacyReleaseUninstall(ctx context.Context, releaseName, releaseNamespace string, opts LegacyReleaseUninstallOptions) error { - legacyUninstallLock.Lock() - defer legacyUninstallLock.Unlock() - - if opts.Timeout == 0 { - return legacyReleaseUninstall(ctx, releaseName, releaseNamespace, opts) - } - - ctx, ctxCancelFn := context.WithTimeoutCause(ctx, opts.Timeout, fmt.Errorf("context timed out: action timed out after %s", opts.Timeout.String())) - defer ctxCancelFn() - - actionCh := make(chan error, 1) - go func() { - actionCh <- legacyReleaseUninstall(ctx, releaseName, releaseNamespace, opts) - }() - - for { - select { - case err := <-actionCh: - return err - case <-ctx.Done(): - return context.Cause(ctx) - } - } -} - -func legacyReleaseUninstall(ctx context.Context, releaseName, releaseNamespace string, opts LegacyReleaseUninstallOptions) error { - currentDir, err := os.Getwd() - if err != nil { - return fmt.Errorf("get current working directory: %w", err) - } - - homeDir, err := os.UserHomeDir() - if err != nil { - return fmt.Errorf("get home directory: %w", err) - } - - opts, err = applyLegacyReleaseUninstallOptionsDefaults(opts, currentDir, homeDir) - if err != nil { - return fmt.Errorf("build legacy release uninstall options: %w", err) - } - - if len(opts.KubeConfigPaths) > 0 { - var splitPaths []string - for _, path := range opts.KubeConfigPaths { - splitPaths = append(splitPaths, filepath.SplitList(path)...) - } - - opts.KubeConfigPaths = lo.Compact(splitPaths) - - // Don't even ask... This way we force ClientConfigLoadingRules.ExplicitPath to always be - // empty, otherwise KUBECONFIG with multiple files doesn't work. Eventually should switch - // from Kubedog to Nelm for initializing K8s Clients like in other actions and get rid of - // this. - opts.KubeConfigPaths = append([]string{""}, opts.KubeConfigPaths...) - } - - kubeConfig, err := kube.NewKubeConfig(ctx, opts.KubeConfigPaths, kube.KubeConfigOptions{ - KubeConnectionOptions: opts.KubeConnectionOptions, - KubeContextNamespace: releaseNamespace, - }) - if err != nil { - return fmt.Errorf("construct kube config: %w", err) - } - - clientFactory, err := kube.NewClientFactory(ctx, kubeConfig) - if err != nil { - return fmt.Errorf("construct kube client factory: %w", err) - } - - helmSettings := cli.New() - *helmSettings.GetConfigP() = clientFactory.LegacyClientGetter() - *helmSettings.GetNamespaceP() = releaseNamespace - releaseNamespace = helmSettings.Namespace() - helmSettings.MaxHistory = opts.ReleaseHistoryLimit - helmSettings.Debug = log.Default.AcceptLevel(ctx, log.Level(log.DebugLevel)) - - if opts.KubeContextCurrent != "" { - helmSettings.KubeContext = opts.KubeContextCurrent - } - - var kubeConfigPath string - if len(opts.KubeConfigPaths) > 0 { - kubeConfigPath = opts.KubeConfigPaths[0] - } - - helmSettings.KubeConfig = kubeConfigPath - - if err := kdkube.Init(kdkube.InitOptions{ - KubeConfigOptions: kdkube.KubeConfigOptions{ - Context: opts.KubeContextCurrent, - ConfigPath: kubeConfigPath, - ConfigDataBase64: opts.KubeConfigBase64, - ConfigPathMergeList: opts.KubeConfigPaths, - }, - }); err != nil { - return fmt.Errorf("initialize kubedog kube client: %w", err) - } - - if err := initKubedog(ctx); err != nil { - return fmt.Errorf("initialize kubedog: %w", err) - } - - helmActionConfig := &action.Configuration{} - if err := helmActionConfig.Init( - helmSettings.RESTClientGetter(), - releaseNamespace, - string(opts.ReleaseStorageDriver), - func(format string, a ...interface{}) { - log.Default.Debug(ctx, format, a...) - }, - ); err != nil { - return fmt.Errorf("helm action config init: %w", err) - } - - helmReleaseStorage := helmActionConfig.Releases - helmReleaseStorage.MaxHistory = opts.ReleaseHistoryLimit - - helmKubeClient := helmActionConfig.KubeClient.(*helmkube.Client) - helmKubeClient.Namespace = releaseNamespace - helmKubeClient.ResourcesWaiter = deploy.NewResourcesWaiter( - helmKubeClient, - time.Now(), - opts.ProgressTablePrintInterval, - opts.ProgressTablePrintInterval, - ) - - nsMeta := spec.NewResourceMeta(releaseNamespace, "", releaseNamespace, "", schema.GroupVersionKind{Version: "v1", Kind: "Namespace"}, nil, nil) - - if _, err := clientFactory.KubeClient().Get( - ctx, - nsMeta, - kube.KubeClientGetOptions{ - TryCache: true, - }, - ); err != nil { - if apierrors.IsNotFound(err) { - log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render(fmt.Sprintf("Skipped release %q removal: no release namespace %q found", releaseName, releaseNamespace))) - - return nil - } else { - return fmt.Errorf("get release namespace: %w", err) - } - } - - if err := func() error { - var releaseFound bool - if _, err := helmActionConfig.Releases.History(releaseName); err != nil { - if !errors.Is(err, driver.ErrReleaseNotFound) { - return fmt.Errorf("get release history: %w", err) - } - } else { - releaseFound = true - } - - if !releaseFound { - log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render(fmt.Sprintf("Skipped release %q (namespace: %q) uninstall: no release found", releaseName, releaseNamespace))) - - return nil - } - - log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render("Deleting release")+" %q (namespace: %q)", releaseName, releaseNamespace) - - var lockManager *lock.LockManager - if m, err := lock.NewLockManager(ctx, releaseNamespace, false, clientFactory); err != nil { - return fmt.Errorf("construct lock manager: %w", err) - } else { - lockManager = m - } - - if lock, err := lockManager.LockRelease(ctx, releaseName); err != nil { - return fmt.Errorf("lock release: %w", err) - } else { - defer lockManager.Unlock(lock) - } - - helmUninstallCmd := helmv3.NewUninstallCmd( - helmActionConfig, - os.Stdout, - helmv3.UninstallCmdOptions{ - StagesSplitter: deploy.NewStagesSplitter(), - DeleteHooks: lo.ToPtr(!opts.NoDeleteHooks), - DontFailIfNoRelease: lo.ToPtr(true), - }, - ) - - if err := helmUninstallCmd.RunE(helmUninstallCmd, []string{releaseName}); err != nil { - return fmt.Errorf("run uninstall command: %w", err) - } - - log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render(fmt.Sprintf("Uninstalled release %q (namespace: %q)", releaseName, releaseNamespace))) - - return nil - }(); err != nil { - return err - } - - if opts.DeleteReleaseNamespace { - log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render(fmt.Sprintf("Deleting release namespace %q", releaseNamespace))) - - if err := clientFactory.KubeClient().Delete(ctx, nsMeta, kube.KubeClientDeleteOptions{}); err != nil { - return fmt.Errorf("delete release namespace: %w", err) - } - - log.Default.Info(ctx, color.Style{color.Bold, color.Green}.Render(fmt.Sprintf("Deleted release namespace %q", releaseNamespace))) - } - - return nil -} - -func initKubedog(ctx context.Context) error { - flag.CommandLine.Parse([]string{}) - - display.SetOut(os.Stdout) - display.SetErr(os.Stderr) - - if err := silenceKlog(ctx); err != nil { - return fmt.Errorf("silence klog: %w", err) - } - - if err := silenceKlogV2(ctx); err != nil { - return fmt.Errorf("silence klog v2: %w", err) - } - - return nil -} - -func applyLegacyReleaseUninstallOptionsDefaults(opts LegacyReleaseUninstallOptions, currentDir, homeDir string) (LegacyReleaseUninstallOptions, error) { - var err error - if opts.TempDirPath == "" { - opts.TempDirPath, err = os.MkdirTemp("", "") - if err != nil { - return LegacyReleaseUninstallOptions{}, fmt.Errorf("create temp dir: %w", err) - } - } - - opts.KubeConnectionOptions.ApplyDefaults(homeDir) - opts.TrackingOptions.ApplyDefaults() - - if opts.NetworkParallelism <= 0 { - opts.NetworkParallelism = common.DefaultNetworkParallelism - } - - if opts.ReleaseHistoryLimit <= 0 { - opts.ReleaseHistoryLimit = common.DefaultReleaseHistoryLimit - } - - if opts.ReleaseStorageDriver == common.ReleaseStorageDriverDefault { - opts.ReleaseStorageDriver = common.ReleaseStorageDriverSecrets - } else if opts.ReleaseStorageDriver == common.ReleaseStorageDriverMemory { - return LegacyReleaseUninstallOptions{}, fmt.Errorf("memory release storage driver is not supported") - } - - return opts, nil -} - -func silenceKlog(ctx context.Context) error { - fs := flag.NewFlagSet("klog", flag.PanicOnError) - klog.InitFlags(fs) - - if err := fs.Set("logtostderr", "false"); err != nil { - return fmt.Errorf("set logtostderr: %w", err) - } - - if err := fs.Set("alsologtostderr", "false"); err != nil { - return fmt.Errorf("set alsologtostderr: %w", err) - } - - if err := fs.Set("stderrthreshold", "5"); err != nil { - return fmt.Errorf("set stderrthreshold: %w", err) - } - - // Suppress info and warnings from client-go reflector - klog.SetOutputBySeverity("INFO", ioutil.Discard) - klog.SetOutputBySeverity("WARNING", ioutil.Discard) - klog.SetOutputBySeverity("ERROR", ioutil.Discard) - klog.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) - - return nil -} - -func silenceKlogV2(ctx context.Context) error { - fs := flag.NewFlagSet("klog", flag.PanicOnError) - klogv2.InitFlags(fs) - - if err := fs.Set("logtostderr", "false"); err != nil { - return fmt.Errorf("set logtostderr: %w", err) - } - - if err := fs.Set("alsologtostderr", "false"); err != nil { - return fmt.Errorf("set alsologtostderr: %w", err) - } - - if err := fs.Set("stderrthreshold", "5"); err != nil { - return fmt.Errorf("set stderrthreshold: %w", err) - } - - // Suppress info and warnings from client-go reflector - klogv2.SetOutputBySeverity("INFO", ioutil.Discard) - klogv2.SetOutputBySeverity("WARNING", ioutil.Discard) - klogv2.SetOutputBySeverity("ERROR", ioutil.Discard) - klogv2.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) - - return nil -} diff --git a/pkg/action/setup_logging.go b/pkg/action/setup_logging.go new file mode 100644 index 00000000..f5b02516 --- /dev/null +++ b/pkg/action/setup_logging.go @@ -0,0 +1,179 @@ +package action + +import ( + "context" + "flag" + "fmt" + "io" + stdlog "log" + "os" + + cdlog "github.com/containerd/log" + "github.com/davecgh/go-spew/spew" + "github.com/gookit/color" + "github.com/hofstadter-io/cinful" + "github.com/samber/lo" + "github.com/sirupsen/logrus" + "github.com/xo/terminfo" + "k8s.io/klog" + klogv2 "k8s.io/klog/v2" + + kdlog "github.com/werf/kubedog/pkg/log" + "github.com/werf/logboek" + "github.com/werf/nelm/pkg/helm/pkg/engine" + "github.com/werf/nelm/pkg/log" +) + +type SetupLoggingOptions struct { + ColorMode string + LogIsParseable bool +} + +func SetupLogging(ctx context.Context, logLevel log.Level, opts SetupLoggingOptions) context.Context { + if val := ctx.Value(log.LogboekLoggerCtxKeyName); val == nil { + ctx = logboek.NewContext(ctx, logboek.DefaultLogger()) + } + + log.Default.SetLevel(ctx, logLevel) + + spew.Config.DisablePointerAddresses = true + spew.Config.DisableCapacities = true + + switch logLevel { + case log.SilentLevel, log.ErrorLevel, log.WarningLevel, log.InfoLevel: + stdlog.SetOutput(io.Discard) + + klog.SetOutput(io.Discard) + + klogFlags := &flag.FlagSet{} + klog.InitFlags(klogFlags) + lo.Must0(klogFlags.Set("logtostderr", "false")) + lo.Must0(klogFlags.Set("alsologtostderr", "false")) + lo.Must0(klogFlags.Set("stderrthreshold", "4")) + + klogv2.SetOutput(io.Discard) + + klogV2Flags := &flag.FlagSet{} + klogv2.InitFlags(klogV2Flags) + lo.Must0(klogV2Flags.Set("logtostderr", "false")) + lo.Must0(klogV2Flags.Set("alsologtostderr", "false")) + lo.Must0(klogV2Flags.Set("stderrthreshold", "4")) + + logrus.SetOutput(io.Discard) + + cdlog.L.Logger.SetOutput(io.Discard) + + engine.Debug = false + + kdlog.SetDebug(false) + case log.DebugLevel: + stdlog.SetOutput(os.Stdout) + + klog.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) + klog.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) + klog.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) + klog.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) + + klogv2.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) + klogv2.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) + klogv2.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) + klogv2.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) + + logrus.SetOutput(logboek.Context(ctx).OutStream()) + logrus.SetLevel(logrus.DebugLevel) + + cdlog.L.Logger.SetOutput(logboek.Context(ctx).OutStream()) + cdlog.L.Logger.SetLevel(logrus.DebugLevel) + + engine.Debug = true + + kdlog.SetDebug(true) + case log.TraceLevel: + stdlog.SetOutput(os.Stdout) + + klog.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) + klog.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) + klog.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) + klog.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) + + klogv2.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) + klogv2.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) + klogv2.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) + klogv2.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) + + logrus.SetOutput(logboek.Context(ctx).OutStream()) + logrus.SetLevel(logrus.TraceLevel) + + cdlog.L.Logger.SetOutput(logboek.Context(ctx).OutStream()) + cdlog.L.Logger.SetLevel(logrus.TraceLevel) + + engine.Debug = true + + kdlog.SetDebug(true) + default: + panic(fmt.Sprintf("unknown log level %q", logLevel)) + } + + colorLevel := getColorLevel(opts.ColorMode, opts.LogIsParseable) + + color.Enable = colorLevel != terminfo.ColorLevelNone + color.ForceSetColorLevel(colorLevel) + + return ctx +} + +func getColorLevel(mode string, logIsParseable bool) terminfo.ColorLevel { + switch mode { + case log.LogColorModeOff: + return terminfo.ColorLevelNone + case log.LogColorModeOn: + if colorLevel := color.DetectColorLevel(); colorLevel == terminfo.ColorLevelNone { + return terminfo.ColorLevelHundreds + } else { + return colorLevel + } + } + + if ciInfo := cinful.Info(); ciInfo != nil { + switch ciInfo.Constant { + case "GITLAB", "GITHUB_ACTIONS": + if logIsParseable { + return terminfo.ColorLevelNone + } else { + return terminfo.ColorLevelHundreds + } + case "JENKINS": + if logIsParseable { + return terminfo.ColorLevelNone + } else { + switch os.Getenv("TERM") { + case "xterm", "vga", "gnome-terminal", "css": + return terminfo.ColorLevelHundreds + } + } + default: + if logIsParseable { + return terminfo.ColorLevelNone + } else { + return color.DetectColorLevel() + } + } + } + + if piped, err := stdoutPiped(); err != nil || piped { + return terminfo.ColorLevelNone + } + + return color.DetectColorLevel() +} + +func stdoutPiped() (bool, error) { + fileInfo, err := os.Stdout.Stat() + if err != nil { + return false, fmt.Errorf("get stdout fileinfo: %w", err) + } + + piped := (fileInfo.Mode() & os.ModeCharDevice) == 0 + + return piped, nil +} diff --git a/pkg/chart/chart_download.go b/pkg/chart/chart_download.go index 6bec0e25..ac7f7964 100644 --- a/pkg/chart/chart_download.go +++ b/pkg/chart/chart_download.go @@ -8,13 +8,11 @@ import ( "os" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" - "github.com/werf/nelm/pkg/helm/pkg/cli" helmdownloader "github.com/werf/nelm/pkg/helm/pkg/downloader" helmgetter "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/helmpath" helmregistry "github.com/werf/nelm/pkg/helm/pkg/registry" - helmrepo "github.com/werf/nelm/pkg/helm/pkg/repo" + helmrepo "github.com/werf/nelm/pkg/helm/pkg/repo/v1" "github.com/werf/nelm/pkg/log" ) @@ -27,7 +25,7 @@ type chartDownloaderOptions struct { } func downloadChart(ctx context.Context, chartPath string, registryClient *helmregistry.Client, opts RenderChartOptions) (string, error) { - if (featgate.FeatGateRemoteCharts.Enabled() || featgate.FeatGatePreviewV2.Enabled()) && !isLocalChart(chartPath) { + if !isLocalChart(chartPath) { chartDownloader, chartRef, err := newChartDownloader(ctx, chartPath, registryClient, chartDownloaderOptions{ ChartRepoConnectionOptions: opts.ChartRepoConnectionOptions, ChartProvenanceKeyring: opts.ChartProvenanceKeyring, @@ -38,13 +36,11 @@ func downloadChart(ctx context.Context, chartPath string, registryClient *helmre return "", fmt.Errorf("construct chart downloader: %w", err) } - // TODO(major): get rid of HELM_ env vars support - if err := os.MkdirAll(cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), 0o755); err != nil { + if err := os.MkdirAll(helmpath.CachePath("repository"), 0o755); err != nil { return "", fmt.Errorf("create repository cache directory: %w", err) } - // TODO(major): get rid of HELM_ env vars support - chartPath, _, err = chartDownloader.DownloadTo(chartRef, opts.ChartVersion, cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository"))) + chartPath, _, err = chartDownloader.DownloadTo(chartRef, opts.ChartVersion, helmpath.CachePath("repository")) if err != nil { return "", fmt.Errorf("download chart %q: %w", chartRef, err) } @@ -64,9 +60,9 @@ func newChartDownloader(ctx context.Context, chartRef string, registryClient *he downloader := &helmdownloader.ChartDownloader{ Out: out, - Verify: helmdownloader.VerificationStrategyString(opts.ChartProvenanceStrategy).ToVerificationStrategy(), + Verify: parseVerificationStrategy(opts.ChartProvenanceStrategy), Keyring: opts.ChartProvenanceKeyring, - Getters: helmgetter.Providers{helmgetter.HttpProvider, helmgetter.OCIProvider}, + Getters: helmgetter.Getters(), Options: []helmgetter.Option{ helmgetter.WithPassCredentialsAll(opts.ChartRepoPassCreds), helmgetter.WithTLSClientConfig(opts.ChartRepoCertPath, opts.ChartRepoKeyPath, opts.ChartRepoCAPath), @@ -75,15 +71,19 @@ func newChartDownloader(ctx context.Context, chartRef string, registryClient *he helmgetter.WithRegistryClient(registryClient), helmgetter.WithTimeout(opts.ChartRepoRequestTimeout), }, - RegistryClient: registryClient, - // TODO(major): get rid of HELM_ env vars support - RepositoryConfig: cli.EnvOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), - // TODO(major): get rid of HELM_ env vars support - RepositoryCache: cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), + RegistryClient: registryClient, + RepositoryConfig: helmpath.ConfigPath("repositories.yaml"), + RepositoryCache: helmpath.CachePath("repository"), } if opts.ChartRepoURL != "" { - chartURL, err := helmrepo.FindChartInAuthAndTLSAndPassRepoURL(opts.ChartRepoURL, opts.ChartRepoBasicAuthUsername, opts.ChartRepoBasicAuthPassword, chartRef, opts.ChartVersion, opts.ChartRepoCertPath, opts.ChartRepoKeyPath, opts.ChartRepoCAPath, opts.ChartRepoSkipTLSVerify, opts.ChartRepoPassCreds, helmgetter.Providers{helmgetter.HttpProvider, helmgetter.OCIProvider}) + chartURL, err := helmrepo.FindChartInRepoURL(opts.ChartRepoURL, chartRef, helmgetter.Getters(), + helmrepo.WithChartVersion(opts.ChartVersion), + helmrepo.WithUsernamePassword(opts.ChartRepoBasicAuthUsername, opts.ChartRepoBasicAuthPassword), + helmrepo.WithClientTLS(opts.ChartRepoCertPath, opts.ChartRepoKeyPath, opts.ChartRepoCAPath), + helmrepo.WithInsecureSkipTLSVerify(opts.ChartRepoSkipTLSVerify), + helmrepo.WithPassCredentialsAll(opts.ChartRepoPassCreds), + ) if err != nil { return nil, "", fmt.Errorf("get chart URL: %w", err) } diff --git a/pkg/chart/chart_render.go b/pkg/chart/chart_render.go index 43ba1a46..4666957e 100644 --- a/pkg/chart/chart_render.go +++ b/pkg/chart/chart_render.go @@ -18,24 +18,28 @@ import ( "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/featgate" + v3chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + chartv3util "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" "github.com/werf/nelm/pkg/helm/pkg/action" helmchart "github.com/werf/nelm/pkg/helm/pkg/chart" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chartcommonutil "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/cli" + v2chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartv2util "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/cli/values" helmdownloader "github.com/werf/nelm/pkg/helm/pkg/downloader" helmengine "github.com/werf/nelm/pkg/helm/pkg/engine" "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" + releaseutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" "github.com/werf/nelm/pkg/helm/pkg/strvals" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/resource/spec" "github.com/werf/nelm/pkg/ts" + "github.com/werf/nelm/pkg/util" ) type RenderChartOptions struct { @@ -48,7 +52,7 @@ type RenderChartOptions struct { ChartVersion string DenoBinaryPath string ExtraAPIVersions []string - HelmOptions helmopts.HelmOptions + HelmOptions common.HelmOptions IgnoreBundleJS bool LocalKubeVersion string NoStandaloneCRDs bool @@ -59,7 +63,7 @@ type RenderChartOptions struct { } type RenderChartResult struct { - Chart *helmchart.Chart + Chart *v2chart.Chart Notes string ReleaseConfig map[string]interface{} ResourceSpecs []*spec.ResourceSpec @@ -81,22 +85,23 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s } depDownloader := &helmdownloader.Manager{ - Out: os.Stdout, - ChartPath: chartPath, - Verify: helmdownloader.VerificationStrategyString(opts.ChartProvenanceStrategy).ToVerificationStrategy(), - Debug: log.Default.AcceptLevel(ctx, log.DebugLevel), - Keyring: opts.ChartProvenanceKeyring, - SkipUpdate: opts.ChartRepoNoUpdate, - Getters: getter.Providers{getter.HttpProvider, getter.OCIProvider}, - RegistryClient: registryClient, - // TODO(major): don't read HELM_REPOSITORY_CONFIG anymore - RepositoryConfig: cli.EnvOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), - // TODO(major): don't read HELM_REPOSITORY_CACHE anymore - RepositoryCache: cli.EnvOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), + Out: os.Stdout, + ChartPath: chartPath, + Verify: parseVerificationStrategy(opts.ChartProvenanceStrategy), + Debug: log.Default.AcceptLevel(ctx, log.DebugLevel), + Keyring: opts.ChartProvenanceKeyring, + SkipUpdate: opts.ChartRepoNoUpdate, + Getters: getter.Getters(), + RegistryClient: registryClient, + RepositoryConfig: helmpath.ConfigPath("repositories.yaml"), + RepositoryCache: helmpath.CachePath("repository"), + ContentCache: helmpath.CachePath("content"), AllowMissingRepos: true, } - opts.HelmOptions.ChartLoadOpts.DepDownloader = depDownloader + opts.HelmOptions.ChartLoadOpts.ChartDepsDownloader = depDownloader + + ctx = common.ContextWithHelmOptions(ctx, opts.HelmOptions) overrideValuesOpts := &values.Options{ ValueFiles: opts.ValuesFiles, @@ -110,7 +115,7 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s log.Default.TraceStruct(ctx, overrideValuesOpts, "Override values options:") log.Default.Debug(ctx, "Merging override values for chart at %q", chartPath) - overrideValues, err := overrideValuesOpts.MergeValues(getter.Providers{getter.HttpProvider, getter.OCIProvider}, opts.HelmOptions) + overrideValues, err := overrideValuesOpts.MergeValues(ctx, getter.Getters()) if err != nil { return nil, fmt.Errorf("merge override values for chart at %q: %w", chartPath, err) } @@ -118,52 +123,77 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s log.Default.TraceStruct(ctx, overrideValues, "Merged override values:") log.Default.Debug(ctx, "Loading chart at %q", chartPath) - chart, err := loader.Load(chartPath, opts.HelmOptions) + loadedChart, err := loader.Load(ctx, chartPath) if err != nil { return nil, fmt.Errorf("load chart at %q: %w", chartPath, err) } - if err := validateChart(ctx, chart); err != nil { + var ( + chartV2 *v2chart.Chart + chartV3 *v3chart.Chart + ) + + switch c := loadedChart.(type) { + case *v2chart.Chart: + chartV2 = c + case *v3chart.Chart: + chartV3 = c + default: + return nil, fmt.Errorf("loaded chart has unexpected type %T", loadedChart) + } + + chartAccessor, err := helmchart.NewAccessor(loadedChart) + if err != nil { + return nil, fmt.Errorf("create chart accessor: %w", err) + } + + if err := validateChart(ctx, loadedChart, chartAccessor); err != nil { return nil, fmt.Errorf("validate chart at %q: %w", chartPath, err) } - log.Default.TraceStruct(ctx, chart, "Chart:") + log.Default.TraceStruct(ctx, loadedChart, "Chart:") - if err := chartutil.ProcessDependenciesWithMerge(chart, &overrideValues); err != nil { - return nil, fmt.Errorf("process chart %q dependencies: %w", chart.Name(), err) + if chartV2 != nil { + if err := chartv2util.ProcessDependencies(chartV2, &overrideValues); err != nil { + return nil, fmt.Errorf("process chart %q dependencies: %w", chartV2.Name(), err) + } + } else { + if err := chartv3util.ProcessDependencies(chartV3, overrideValues); err != nil { + return nil, fmt.Errorf("process chart %q dependencies: %w", chartV3.Name(), err) + } } - log.Default.TraceStruct(ctx, chart, "Chart after processing dependencies:") + log.Default.TraceStruct(ctx, loadedChart, "Chart after processing dependencies:") log.Default.TraceStruct(ctx, overrideValues, "Merged override values after processing dependencies:") + var chartKubeVersion string + if chartV2 != nil { + chartKubeVersion = chartV2.Metadata.KubeVersion + } else { + chartKubeVersion = chartV3.Metadata.KubeVersion + } + caps, err := buildChartCapabilities(ctx, clientFactory, buildChartCapabilitiesOptions{ ExtraAPIVersions: opts.ExtraAPIVersions, LocalKubeVersion: opts.LocalKubeVersion, Remote: opts.Remote, }) if err != nil { - return nil, fmt.Errorf("build capabilities for chart %q: %w", chart.Name(), err) + return nil, fmt.Errorf("build capabilities for chart %q: %w", chartAccessor.Name(), err) } log.Default.TraceStruct(ctx, caps, "Capabilities:") - if chart.Metadata.KubeVersion != "" && !chartutil.IsCompatibleRange(chart.Metadata.KubeVersion, caps.KubeVersion.String()) { - return nil, fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", chart.Metadata.KubeVersion, caps.KubeVersion.String()) - } - - runtime, err := buildContextFromJSONSets(opts.RuntimeSetJSON) - if err != nil { - return nil, fmt.Errorf("build runtime: %w", err) + if chartKubeVersion != "" && !chartv2util.IsCompatibleRange(chartKubeVersion, caps.KubeVersion.String()) { + return nil, fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", chartKubeVersion, caps.KubeVersion.String()) } - log.Default.TraceStruct(ctx, runtime, "Runtime:") - - opts.HelmOptions.ChartLoadOpts.DefaultRootContext, err = buildContextFromJSONSets(opts.RootSetJSON) + defaultRootContext, err := buildContextFromJSONSets(opts.RootSetJSON) if err != nil { return nil, fmt.Errorf("build default root context: %w", err) } - log.Default.TraceStruct(ctx, opts.HelmOptions.ChartLoadOpts.DefaultRootContext, "Default root context:") + log.Default.TraceStruct(ctx, defaultRootContext, "Default root context:") var isUpgrade bool @@ -178,15 +208,21 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s log.Default.Debug(ctx, "Rendering values for chart at %q", chartPath) - renderedValues, err := chartutil.ToRenderValues(chart, overrideValues, chartutil.ReleaseOptions{ + renderedValues, err := chartcommonutil.ToRenderValues(loadedChart, overrideValues, chartcommon.ReleaseOptions{ Name: releaseName, Namespace: releaseNamespace, Revision: revision, IsInstall: !isUpgrade, IsUpgrade: isUpgrade, - }, caps, runtime, opts.HelmOptions.ChartLoadOpts.DefaultRootContext) + }, caps) if err != nil { - return nil, fmt.Errorf("build rendered values for chart %q: %w", chart.Name(), err) + return nil, fmt.Errorf("build rendered values for chart %q: %w", chartAccessor.Name(), err) + } + + for k, v := range defaultRootContext { + if _, exists := renderedValues[k]; !exists { + renderedValues[k] = v + } } log.Default.TraceStruct(ctx, renderedValues.AsMap(), "Rendered values:") @@ -205,11 +241,28 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s var resources []*spec.ResourceSpec if !opts.NoStandaloneCRDs { - for _, crd := range chart.CRDObjects() { - for _, manifest := range releaseutil.SplitManifestsToSlice(string(crd.File.Data)) { + type crdRef struct { + data []byte + filename string + } + + var crds []crdRef + + if chartV2 != nil { + for _, crd := range chartV2.CRDObjects() { + crds = append(crds, crdRef{data: crd.File.Data, filename: crd.Filename}) + } + } else { + for _, crd := range chartV3.CRDObjects() { + crds = append(crds, crdRef{data: crd.File.Data, filename: crd.Filename}) + } + } + + for _, crd := range crds { + for _, manifest := range util.SplitManifests(string(crd.data)) { if res, err := spec.NewResourceSpecFromManifest(manifest, releaseNamespace, spec.ResourceSpecOptions{ StoreAs: common.StoreAsNone, - FilePath: crd.Filename, + FilePath: crd.filename, }); err != nil { return nil, fmt.Errorf("construct standalone CRD for chart at %q: %w", chartPath, err) } else { @@ -219,17 +272,25 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s } } - renderedTemplates, err := engine.Render(chart, renderedValues, opts.HelmOptions) + renderedTemplates, err := engine.Render(ctx, loadedChart, renderedValues) if err != nil { - return nil, fmt.Errorf("render resources for chart %q: %w", chart.Name(), err) + return nil, fmt.Errorf("render resources for chart %q: %w", chartAccessor.Name(), err) } if featgate.FeatGateTypescript.Enabled() { - log.Default.Debug(ctx, "Rendering TypeScript resources for chart %q and its dependencies", chart.Name()) + var tsChart *v2chart.Chart + if chartV2 != nil { + tsChart = chartV2 + } else { + // TODO(major): refactor to allow native v3 chart handling in TypeScript rendering + tsChart = convertV3ToV2(chartV3) + } + + log.Default.Debug(ctx, "Rendering TypeScript resources for chart %q and its dependencies", tsChart.Name()) - jsRenderedTemplates, err := ts.RenderChart(ctx, chart, renderedValues, opts.IgnoreBundleJS, chartPath, opts.TempDirPath, opts.DenoBinaryPath) + jsRenderedTemplates, err := ts.RenderChart(ctx, tsChart, renderedValues, opts.IgnoreBundleJS, chartPath, opts.TempDirPath, opts.DenoBinaryPath) if err != nil { - return nil, fmt.Errorf("render TypeScript templates for chart %q: %w", chart.Name(), err) + return nil, fmt.Errorf("render TypeScript templates for chart %q: %w", chartAccessor.Name(), err) } if len(jsRenderedTemplates) > 0 { @@ -241,7 +302,7 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s for filePath, fileContent := range renderedTemplates { if strings.HasPrefix(path.Base(filePath), "_") || - strings.HasSuffix(filePath, action.NotesFileSuffix) || + strings.HasSuffix(filePath, "NOTES.txt") || strings.TrimSpace(fileContent) == "" { continue } @@ -255,7 +316,7 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s resources = append(resources, r...) } - notes := buildChartNotes(chart.Name(), renderedTemplates, opts.SubchartNotes) + notes := buildChartNotes(chartAccessor.Name(), renderedTemplates, opts.SubchartNotes) log.Default.TraceStruct(ctx, notes, "Rendered notes:") @@ -263,8 +324,16 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s return spec.ResourceSpecSortHandler(resources[i], resources[j]) }) + var resultChart *v2chart.Chart + if chartV2 != nil { + resultChart = chartV2 + } else { + // TODO(major): refactor to allow native v3 chart handling in nelm + resultChart = convertV3ToV2(chartV3) + } + return &RenderChartResult{ - Chart: chart, + Chart: resultChart, Notes: notes, ReleaseConfig: overrideValues, ResourceSpecs: resources, @@ -272,9 +341,85 @@ func RenderChart(ctx context.Context, chartPath, releaseName, releaseNamespace s }, nil } -func buildChartCapabilities(ctx context.Context, clientFactory kube.ClientFactorier, opts buildChartCapabilitiesOptions) (*chartutil.Capabilities, error) { - capabilities := &chartutil.Capabilities{ - HelmVersion: chartutil.DefaultCapabilities.HelmVersion, +func convertV3ToV2(src *v3chart.Chart) *v2chart.Chart { + dst := &v2chart.Chart{ + Raw: src.Raw, + Templates: src.Templates, + Values: src.Values, + Schema: src.Schema, + SchemaModTime: src.SchemaModTime, + Files: src.Files, + ModTime: src.ModTime, + RuntimeFiles: src.RuntimeFiles, + ExtraValues: src.ExtraValues, + SecretsRuntimeData: src.SecretsRuntimeData, + } + + if src.Metadata != nil { + dst.Metadata = convertV3MetadataToV2(src.Metadata) + } + + if src.Lock != nil { + dst.Lock = convertV3LockToV2(src.Lock) + } + + for _, dep := range src.Dependencies() { + dst.AddDependency(convertV3ToV2(dep)) + } + + return dst +} + +func convertV3LockToV2(src *v3chart.Lock) *v2chart.Lock { + dst := &v2chart.Lock{ + Generated: src.Generated, + Digest: src.Digest, + } + + for _, dependency := range src.Dependencies { + dst.Dependencies = append(dst.Dependencies, convertV3DependencyToV2(dependency)) + } + + return dst +} + +func convertV3MetadataToV2(src *v3chart.Metadata) *v2chart.Metadata { + dst := &v2chart.Metadata{ + Name: src.Name, + Home: src.Home, + Sources: src.Sources, + Version: src.Version, + Description: src.Description, + Keywords: src.Keywords, + Icon: src.Icon, + APIVersion: src.APIVersion, + Condition: src.Condition, + Tags: src.Tags, + AppVersion: src.AppVersion, + Deprecated: src.Deprecated, + Annotations: src.Annotations, + KubeVersion: src.KubeVersion, + Type: src.Type, + } + + for _, maintainer := range src.Maintainers { + dst.Maintainers = append(dst.Maintainers, &v2chart.Maintainer{ + Name: maintainer.Name, + Email: maintainer.Email, + URL: maintainer.URL, + }) + } + + for _, dependency := range src.Dependencies { + dst.Dependencies = append(dst.Dependencies, convertV3DependencyToV2(dependency)) + } + + return dst +} + +func buildChartCapabilities(ctx context.Context, clientFactory kube.ClientFactorier, opts buildChartCapabilitiesOptions) (*chartcommon.Capabilities, error) { + capabilities := &chartcommon.Capabilities{ + HelmVersion: chartcommon.DefaultCapabilities.HelmVersion, } if opts.Remote { @@ -285,7 +430,7 @@ func buildChartCapabilities(ctx context.Context, clientFactory kube.ClientFactor return nil, fmt.Errorf("get kubernetes server version: %w", err) } - capabilities.KubeVersion = chartutil.KubeVersion{ + capabilities.KubeVersion = chartcommon.KubeVersion{ Version: kubeVersion.GitVersion, Major: kubeVersion.Major, Minor: kubeVersion.Minor, @@ -303,21 +448,21 @@ func buildChartCapabilities(ctx context.Context, clientFactory kube.ClientFactor capabilities.APIVersions = apiVersions } else { if opts.LocalKubeVersion != "" { - kubeVersion, err := chartutil.ParseKubeVersion(opts.LocalKubeVersion) + kubeVersion, err := chartcommon.ParseKubeVersion(opts.LocalKubeVersion) if err != nil { return nil, fmt.Errorf("parse kube version %q: %w", opts.LocalKubeVersion, err) } capabilities.KubeVersion = *kubeVersion } else { - capabilities.KubeVersion = chartutil.DefaultCapabilities.KubeVersion + capabilities.KubeVersion = chartcommon.DefaultCapabilities.KubeVersion } - capabilities.APIVersions = chartutil.DefaultCapabilities.APIVersions + capabilities.APIVersions = chartcommon.DefaultCapabilities.APIVersions } if opts.ExtraAPIVersions != nil { - capabilities.APIVersions = append(capabilities.APIVersions, chartutil.VersionSet(opts.ExtraAPIVersions)...) + capabilities.APIVersions = append(capabilities.APIVersions, chartcommon.VersionSet(opts.ExtraAPIVersions)...) } return capabilities, nil @@ -327,7 +472,7 @@ func buildChartNotes(chartName string, renderedTemplates map[string]string, rend var resultBuf bytes.Buffer for filePath, fileContent := range renderedTemplates { - if !strings.HasSuffix(filePath, action.NotesFileSuffix) { + if !strings.HasSuffix(filePath, "NOTES.txt") { continue } @@ -336,7 +481,7 @@ func buildChartNotes(chartName string, renderedTemplates map[string]string, rend continue } - isTopLevelNotes := filePath == path.Join(chartName, "templates", action.NotesFileSuffix) + isTopLevelNotes := filePath == path.Join(chartName, "templates", "NOTES.txt") if !isTopLevelNotes && !renderSubchartNotes { continue @@ -364,21 +509,47 @@ func buildContextFromJSONSets(jsonSets []string) (map[string]interface{}, error) return context, nil } +func convertV3DependencyToV2(src *v3chart.Dependency) *v2chart.Dependency { + return &v2chart.Dependency{ + Name: src.Name, + Version: src.Version, + Repository: src.Repository, + Condition: src.Condition, + Tags: src.Tags, + Enabled: src.Enabled, + ImportValues: src.ImportValues, + Alias: src.Alias, + } +} + func isLocalChart(path string) bool { return filepath.IsAbs(path) || filepath.HasPrefix(path, "..") || filepath.HasPrefix(path, ".") } +func parseVerificationStrategy(s string) helmdownloader.VerificationStrategy { + switch s { + case "verify": + return helmdownloader.VerifyAlways + case "verify-if-possible": + return helmdownloader.VerifyIfPossible + case "later": + return helmdownloader.VerifyLater + default: + return helmdownloader.VerifyNever + } +} + func renderedTemplatesToResourceSpecs(renderedTemplates map[string]string, releaseNamespace string, opts RenderChartOptions) ([]*spec.ResourceSpec, error) { var resources []*spec.ResourceSpec for filePath, fileContent := range renderedTemplates { if strings.HasPrefix(path.Base(filePath), "_") || - strings.HasSuffix(filePath, action.NotesFileSuffix) || + strings.HasSuffix(filePath, "NOTES.txt") || strings.TrimSpace(fileContent) == "" { continue } - manifests := releaseutil.SplitManifestsToSlice(fileContent) + manifests := util.SplitManifests(fileContent) for idx, manifest := range manifests { var head releaseutil.SimpleHead @@ -404,23 +575,27 @@ func renderedTemplatesToResourceSpecs(renderedTemplates map[string]string, relea return resources, nil } -func validateChart(ctx context.Context, chart *helmchart.Chart) error { - if chart == nil { - return fmt.Errorf("load chart: %w", action.ErrMissingChart()) +func validateChart(ctx context.Context, chrt helmchart.Charter, acc helmchart.Accessor) error { + if chrt == nil { + return fmt.Errorf("load chart: missing chart") } - if chart.Metadata.Type != "" && chart.Metadata.Type != "application" { - return fmt.Errorf("chart %q of type %q can't be deployed", chart.Name(), chart.Metadata.Type) + meta := acc.MetadataAsMap() + + chartType, _ := meta["Type"].(string) + if chartType != "" && chartType != "application" { + return fmt.Errorf("chart %q of type %q can't be deployed", acc.Name(), chartType) } - if chart.Metadata.Dependencies != nil { - if err := action.CheckDependencies(chart, chart.Metadata.Dependencies); err != nil { - return fmt.Errorf("check chart dependencies for chart %q: %w", chart.Name(), err) + if metaDeps := acc.MetaDependencies(); len(metaDeps) > 0 { + if err := action.CheckDependencies(chrt, metaDeps); err != nil { + return fmt.Errorf("check chart dependencies for chart %q: %w", acc.Name(), err) } } - if chart.Metadata.Deprecated { - log.Default.Warn(ctx, `Chart "%s:%s" is deprecated`, chart.Name(), chart.Metadata.Version) + if acc.Deprecated() { + chartVersion, _ := meta["Version"].(string) + log.Default.Warn(ctx, `Chart "%s:%s" is deprecated`, acc.Name(), chartVersion) } return nil diff --git a/pkg/common/common.go b/pkg/common/common.go index 7ca7f89c..e4418d7a 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -1,18 +1,19 @@ package common import ( + "context" "fmt" + "os" + "os/user" "path/filepath" "regexp" + "runtime" "time" "github.com/Masterminds/sprig/v3" - "github.com/docker/cli/cli/config" - "github.com/docker/docker/pkg/homedir" "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/log" ) @@ -77,6 +78,8 @@ const ( StoreAsHook StoreAs = "hook" StoreAsRegular StoreAs = "regular" + CacheDirAPIResourceJSONSchemas = CacheSubdirNelm + "/api-resource-json-schemas" + CacheSubdirNelm = "nelm" // ChartTSBundleFile is the path to the bundle in a Helm chart. ChartTSBundleFile = ChartTSSourceDir + "dist/bundle.js" // ChartTSEntryPointJS is the JavaScript entry point path. @@ -93,11 +96,10 @@ const ( // TODO(major): switch to if-possible DefaultChartProvenanceStrategy = "never" // TODO(major): reconsider? - DefaultDeletePropagation = metav1.DeletePropagationForeground - DefaultDiffContextLines = 3 - DefaultFieldManager = "helm" - // TODO(major): update to a more recent version? Not sure about backwards compatibility. - DefaultLocalKubeVersion = "1.20.0" + DefaultDeletePropagation = metav1.DeletePropagationForeground + DefaultDiffContextLines = 3 + DefaultFieldManager = "helm" + DefaultLocalKubeVersion = "1.36.0" DefaultLogColorMode = log.LogColorModeAuto DefaultNetworkParallelism = 30 DefaultProgressPrintInterval = 5 * time.Second @@ -150,8 +152,9 @@ var ( StagePostPostUninstall, StageFinal, } - OrderedStoreAs = []StoreAs{StoreAsNone, StoreAsHook, StoreAsRegular} - DefaultRegistryCredentialsPath = filepath.Join(homedir.Get(), ".docker", config.ConfigFileName) + OrderedStoreAs = []StoreAs{StoreAsNone, StoreAsHook, StoreAsRegular} + // TODO(major): now it respects DOCKER_CONFIG? Is it a breaking change? Anyways, I feel like it shouldn't be a constant, but a proper option for actions + DefaultRegistryCredentialsPath = filepath.Join(dockerConfigDir(), "config.json") LabelKeyHumanManagedBy = "app.kubernetes.io/managed-by" LabelKeyPatternManagedBy = regexp.MustCompile(`^app.kubernetes.io/managed-by$`) AnnotationKeyHumanReleaseName = "meta.helm.sh/release-name" @@ -205,8 +208,6 @@ var ( AnnotationKeyHumanDeleteDependency = "werf.io/delete-dependency-" AnnotationKeyPatternDeleteDependency = regexp.MustCompile(`^werf.io/delete-dependency-(?P.+)$`) // TODO(major): get rid - AnnotationKeyHumanDependency = ".dependency.werf.io" - AnnotationKeyPatternDependency = regexp.MustCompile(`^(?P.+).dependency.werf.io$`) AnnotationKeyHumanExternalDependency = ".external-dependency.werf.io" AnnotationKeyPatternExternalDependency = regexp.MustCompile(`^(?P.+).external-dependency.werf.io$`) AnnotationKeyHumanLegacyExternalDependencyResource = ".external-dependency.werf.io/resource" @@ -229,8 +230,7 @@ var ( "https://raw.githubusercontent.com/yannh/kubernetes-json-schema/master/{{ .NormalizedKubernetesVersion }}-standalone{{ .StrictSuffix }}/{{ .ResourceKind }}{{ .KindSuffix }}.json", "https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json", } - DefaultResourceValidationCacheLifetime = 48 * time.Hour - APIResourceValidationJSONSchemasCacheDir = helmpath.CachePath("nelm", "api-resource-json-schemas") + DefaultResourceValidationCacheLifetime = 48 * time.Hour ) // Type of the current operation. @@ -254,6 +254,49 @@ type ResourceState string // How the resource should be stored in the Helm release. type StoreAs string +type HelmOptions struct { + ChartLoadOpts ChartLoadOptions + TypeScriptOpts TypeScriptOptions +} + +type ChartLoadOptions struct { + ChartAppVersion string + ChartDepsDownloader ChartDepsDownloader + ChartType LegacyChartType + DefaultChartAPIVersion string + DefaultChartName string + DefaultChartVersion string + DefaultSecretValuesDisable bool + DefaultValuesDisable bool + ExtraValues map[string]interface{} + NoSecrets bool + SecretKeyIgnore bool + SecretValuesFiles []string + SecretWorkDir string +} + +type TypeScriptOptions struct { + DenoBinaryPath string +} + +type helmOptionsContextKey struct{} + +func ContextWithHelmOptions(ctx context.Context, opts HelmOptions) context.Context { + return context.WithValue(ctx, helmOptionsContextKey{}, opts) +} + +func HasHelmOptions(ctx context.Context) bool { + _, ok := ctx.Value(helmOptionsContextKey{}).(HelmOptions) + + return ok +} + +func HelmOptionsFromContext(ctx context.Context) HelmOptions { + opts, _ := ctx.Value(helmOptionsContextKey{}).(HelmOptions) + + return opts +} + func StagesSortHandler(stage1, stage2 Stage) bool { index1 := lo.IndexOf(StagesOrdered, stage1) index2 := lo.IndexOf(StagesOrdered, stage2) @@ -264,3 +307,22 @@ func StagesSortHandler(stage1, stage2 Stage) bool { func SubStageWeighted(stage Stage, weight int) Stage { return Stage(fmt.Sprintf("%s/weight:%d", stage, weight)) } + +func dockerConfigDir() string { + if d := os.Getenv("DOCKER_CONFIG"); d != "" { + return d + } + + return filepath.Join(userHomeDir(), ".docker") +} + +func userHomeDir() string { + home, _ := os.UserHomeDir() + if home == "" && runtime.GOOS != "windows" { + if u, err := user.Current(); err == nil { + return u.HomeDir + } + } + + return home +} diff --git a/pkg/common/legacy.go b/pkg/common/legacy.go new file mode 100644 index 00000000..3ded65e8 --- /dev/null +++ b/pkg/common/legacy.go @@ -0,0 +1,45 @@ +package common + +import "context" + +const ( + LegacyChartTypeChart LegacyChartType = "" + LegacyChartTypeBundle LegacyChartType = "bundle" + LegacyChartTypeSubchart LegacyChartType = "subchart" + LegacyChartTypeChartStub LegacyChartType = "chartstub" +) + +var ( + ChartFileReader ChartFileReaderer + + ChartFileWriter ChartFileWriterer + + LegacyCoalesceTablesFunc func(dst, src map[string]interface{}) map[string]interface{} +) + +type LegacyChartType string + +type ChartDepsDownloader interface { + Build(ctx context.Context) error + Update(ctx context.Context) error + UpdateRepositories(ctx context.Context) error + SetChartPath(path string) +} + +type ChartFileReaderer interface { + LocateChart(ctx context.Context, name string) (string, error) + ReadChartFile(ctx context.Context, filePath string) ([]byte, error) + ChartFileExists(ctx context.Context, filePath string) (bool, error) + LoadChartDir(ctx context.Context, dir string) ([]*BufferedFile, error) + ChartIsDir(relPath string) (bool, error) +} + +type ChartFileWriterer interface { + WriteChartFile(ctx context.Context, filePath string, data []byte) error + CreateChartDir(ctx context.Context, dir string) error +} + +type BufferedFile struct { + Data []byte + Name string +} diff --git a/pkg/common/options.go b/pkg/common/options.go index afe3931c..21b477bf 100644 --- a/pkg/common/options.go +++ b/pkg/common/options.go @@ -74,7 +74,14 @@ type KubeConnectionOptions struct { } func (opts *KubeConnectionOptions) ApplyDefaults(homeDir string) { - if opts.KubeConfigBase64 == "" && len(lo.Compact(opts.KubeConfigPaths)) == 0 { + if len(lo.Compact(opts.KubeConfigPaths)) > 0 { + var splitPaths []string + for _, path := range opts.KubeConfigPaths { + splitPaths = append(splitPaths, filepath.SplitList(path)...) + } + + opts.KubeConfigPaths = lo.Compact(splitPaths) + } else if opts.KubeConfigBase64 == "" { opts.KubeConfigPaths = []string{filepath.Join(homeDir, ".kube", "config")} } @@ -124,11 +131,6 @@ type ValuesOptions struct { // arbitrary things in the global root context ("$"). This is meant to be // generated programmatically. Do not use it unless you know what you are doing. RootSetJSON []string - // RuntimeSetJSON is a list of key-value pairs in "key=json" format to set in $.Runtime. - // This is meant to be generated programmatically. Users should prefer ValuesSetJSON. - // Example: ["runtime.env=dev", "runtime.timestamp=1234567890"] - // TODO(major): get rid of it - RuntimeSetJSON []string // ValuesFiles is a list of paths to additional values files to merge with chart values. // Files are merged in order, with later files overriding earlier ones. ValuesFiles []string @@ -284,7 +286,6 @@ type ResourceDiffOptions struct { ShowInsignificantDiffs bool ShowSensitiveDiffs bool ShowVerboseCRDDiffs bool - ShowVerboseDiffs bool } func (opts *ResourceDiffOptions) ApplyDefaults() { diff --git a/pkg/export/helm/action/action.go b/pkg/export/helm/action/action.go deleted file mode 100644 index 36a12544..00000000 --- a/pkg/export/helm/action/action.go +++ /dev/null @@ -1,5 +0,0 @@ -package action - -import internal "github.com/werf/nelm/pkg/helm/pkg/action" - -type RESTClientGetter = internal.RESTClientGetter diff --git a/pkg/export/helm/chart/chart.go b/pkg/export/helm/chart/chart.go deleted file mode 100644 index 8c59f517..00000000 --- a/pkg/export/helm/chart/chart.go +++ /dev/null @@ -1,11 +0,0 @@ -package chart - -import internal "github.com/werf/nelm/pkg/helm/pkg/chart" - -const APIVersionV2 = internal.APIVersionV2 - -type Chart = internal.Chart - -type File = internal.File - -type Metadata = internal.Metadata diff --git a/pkg/export/helm/chart/loader/loader.go b/pkg/export/helm/chart/loader/loader.go deleted file mode 100644 index 2d4182a1..00000000 --- a/pkg/export/helm/chart/loader/loader.go +++ /dev/null @@ -1,12 +0,0 @@ -package loader - -import internal "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - -var ( - Load = internal.Load - LoadArchive = internal.LoadArchive - LoadDir = internal.LoadDir - NoChartLockWarning = internal.NoChartLockWarning - SetLocalCacheDir = internal.SetLocalCacheDir - SetServiceDir = internal.SetServiceDir -) diff --git a/pkg/export/helm/chartutil/chartutil.go b/pkg/export/helm/chartutil/chartutil.go deleted file mode 100644 index 22e04348..00000000 --- a/pkg/export/helm/chartutil/chartutil.go +++ /dev/null @@ -1,17 +0,0 @@ -package chartutil - -import internal "github.com/werf/nelm/pkg/helm/pkg/chartutil" - -const ValuesfileName = internal.ValuesfileName - -var ( - CoalesceChartValues = internal.CoalesceChartValues - MergeInternal = internal.MergeInternal - Save = internal.Save - SaveDir = internal.SaveDir - SaveIntoDir = internal.SaveIntoDir - SaveIntoTar = internal.SaveIntoTar - SetGzipWriterMeta = internal.SetGzipWriterMeta -) - -type SaveIntoTarOptions = internal.SaveIntoTarOptions diff --git a/pkg/export/helm/cli/values/values.go b/pkg/export/helm/cli/values/values.go deleted file mode 100644 index 40c138ef..00000000 --- a/pkg/export/helm/cli/values/values.go +++ /dev/null @@ -1,5 +0,0 @@ -package values - -import internal "github.com/werf/nelm/pkg/helm/pkg/cli/values" - -type Options = internal.Options diff --git a/pkg/export/helm/cmd/helm/helm.go b/pkg/export/helm/cmd/helm/helm.go deleted file mode 100644 index 450f0309..00000000 --- a/pkg/export/helm/cmd/helm/helm.go +++ /dev/null @@ -1,13 +0,0 @@ -package helm - -import internal "github.com/werf/nelm/pkg/helm/cmd/helm" - -var Settings = internal.Settings - -func IsPluginError(err error) bool { - return internal.IsPluginError(err) -} - -func PluginErrorCode(err error) int { - return internal.PluginErrorCode(err) -} diff --git a/pkg/export/helm/downloader/downloader.go b/pkg/export/helm/downloader/downloader.go deleted file mode 100644 index 6849b5de..00000000 --- a/pkg/export/helm/downloader/downloader.go +++ /dev/null @@ -1,5 +0,0 @@ -package downloader - -import internal "github.com/werf/nelm/pkg/helm/pkg/downloader" - -type Manager = internal.Manager diff --git a/pkg/export/helm/engine/engine.go b/pkg/export/helm/engine/engine.go deleted file mode 100644 index 0125d670..00000000 --- a/pkg/export/helm/engine/engine.go +++ /dev/null @@ -1,19 +0,0 @@ -package engine - -import internal "github.com/werf/nelm/pkg/helm/pkg/engine" - -func GetDebug() bool { - return internal.Debug -} - -func GetTemplateErrHint() string { - return internal.TemplateErrHint -} - -func SetDebug(v bool) { - internal.Debug = v -} - -func SetTemplateErrHint(v string) { - internal.TemplateErrHint = v -} diff --git a/pkg/export/helm/getter/getter.go b/pkg/export/helm/getter/getter.go deleted file mode 100644 index b4ffc6cc..00000000 --- a/pkg/export/helm/getter/getter.go +++ /dev/null @@ -1,5 +0,0 @@ -package getter - -import internal "github.com/werf/nelm/pkg/helm/pkg/getter" - -var All = internal.All diff --git a/pkg/export/helm/helmpath/helmpath.go b/pkg/export/helm/helmpath/helmpath.go deleted file mode 100644 index 6be8149a..00000000 --- a/pkg/export/helm/helmpath/helmpath.go +++ /dev/null @@ -1,5 +0,0 @@ -package helmpath - -import internal "github.com/werf/nelm/pkg/helm/pkg/helmpath" - -var CachePath = internal.CachePath diff --git a/pkg/export/helm/kube/kube.go b/pkg/export/helm/kube/kube.go deleted file mode 100644 index 88186d9d..00000000 --- a/pkg/export/helm/kube/kube.go +++ /dev/null @@ -1,9 +0,0 @@ -package kube - -import internal "github.com/werf/nelm/pkg/helm/pkg/kube" - -type Client = internal.Client - -type ResourceList = internal.ResourceList - -type ResourcesWaiterDeleteResourceSpec = internal.ResourcesWaiterDeleteResourceSpec diff --git a/pkg/export/helm/phases/stages/externaldeps/externaldeps.go b/pkg/export/helm/phases/stages/externaldeps/externaldeps.go deleted file mode 100644 index 36f560e8..00000000 --- a/pkg/export/helm/phases/stages/externaldeps/externaldeps.go +++ /dev/null @@ -1,9 +0,0 @@ -package externaldeps - -import internal "github.com/werf/nelm/pkg/helm/pkg/phases/stages/externaldeps" - -var NewExternalDependency = internal.NewExternalDependency - -type ExternalDependencyList = internal.ExternalDependencyList - -type GVKBuilder = internal.GVKBuilder diff --git a/pkg/export/helm/phases/stages/stages.go b/pkg/export/helm/phases/stages/stages.go deleted file mode 100644 index 50686060..00000000 --- a/pkg/export/helm/phases/stages/stages.go +++ /dev/null @@ -1,7 +0,0 @@ -package stages - -import internal "github.com/werf/nelm/pkg/helm/pkg/phases/stages" - -type SortedStageList = internal.SortedStageList - -type Stage = internal.Stage diff --git a/pkg/export/helm/postrender/postrender.go b/pkg/export/helm/postrender/postrender.go deleted file mode 100644 index ae62a846..00000000 --- a/pkg/export/helm/postrender/postrender.go +++ /dev/null @@ -1,5 +0,0 @@ -package postrender - -import internal "github.com/werf/nelm/pkg/helm/pkg/postrender" - -type PostRenderer = internal.PostRenderer diff --git a/pkg/export/helm/registry/registry.go b/pkg/export/helm/registry/registry.go deleted file mode 100644 index 70555b55..00000000 --- a/pkg/export/helm/registry/registry.go +++ /dev/null @@ -1,15 +0,0 @@ -package registry - -import internal "github.com/werf/nelm/pkg/helm/pkg/registry" - -var ( - NewClient = internal.NewClient - ClientOptCredentialsFile = internal.ClientOptCredentialsFile - ClientOptDebug = internal.ClientOptDebug - ClientOptPlainHTTP = internal.ClientOptPlainHTTP - ClientOptWriter = internal.ClientOptWriter -) - -type Client = internal.Client - -type ClientOption = internal.ClientOption diff --git a/pkg/export/helm/release/release.go b/pkg/export/helm/release/release.go deleted file mode 100644 index 3ef7cfbd..00000000 --- a/pkg/export/helm/release/release.go +++ /dev/null @@ -1,10 +0,0 @@ -package release - -import internal "github.com/werf/nelm/pkg/helm/pkg/release" - -const ( - StatusDeployed = internal.StatusDeployed - StatusFailed = internal.StatusFailed -) - -type DeployReport = internal.DeployReport diff --git a/pkg/export/helm/releaseutil/releaseutil.go b/pkg/export/helm/releaseutil/releaseutil.go deleted file mode 100644 index a7302745..00000000 --- a/pkg/export/helm/releaseutil/releaseutil.go +++ /dev/null @@ -1,5 +0,0 @@ -package releaseutil - -import internal "github.com/werf/nelm/pkg/helm/pkg/releaseutil" - -var SplitManifests = internal.SplitManifests diff --git a/pkg/export/helm/werf/file/file.go b/pkg/export/helm/werf/file/file.go deleted file mode 100644 index 782b8e95..00000000 --- a/pkg/export/helm/werf/file/file.go +++ /dev/null @@ -1,15 +0,0 @@ -package file - -import internal "github.com/werf/nelm/pkg/helm/pkg/werf/file" - -type ChartExtenderBufferedFile = internal.ChartExtenderBufferedFile - -type ChartFileReaderInterface = internal.ChartFileReaderInterface - -func GetChartFileReader() ChartFileReaderInterface { - return internal.ChartFileReader -} - -func SetChartFileReader(v ChartFileReaderInterface) { - internal.ChartFileReader = v -} diff --git a/pkg/export/helm/werf/helmopts/helmopts.go b/pkg/export/helm/werf/helmopts/helmopts.go deleted file mode 100644 index 7c963e2a..00000000 --- a/pkg/export/helm/werf/helmopts/helmopts.go +++ /dev/null @@ -1,9 +0,0 @@ -package helmopts - -import internal "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" - -const ChartTypeBundle = internal.ChartTypeBundle - -type HelmOptions = internal.HelmOptions - -type ChartLoadOptions = internal.ChartLoadOptions diff --git a/pkg/export/helm/werf/secrets/runtimedata/runtimedata.go b/pkg/export/helm/werf/secrets/runtimedata/runtimedata.go deleted file mode 100644 index 1ada35c3..00000000 --- a/pkg/export/helm/werf/secrets/runtimedata/runtimedata.go +++ /dev/null @@ -1,5 +0,0 @@ -package runtimedata - -import internal "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/runtimedata" - -type RuntimeData = internal.RuntimeData diff --git a/pkg/export/helm/werf/secrets/secrets.go b/pkg/export/helm/werf/secrets/secrets.go deleted file mode 100644 index 43ad3a7f..00000000 --- a/pkg/export/helm/werf/secrets/secrets.go +++ /dev/null @@ -1,5 +0,0 @@ -package secrets - -import internal "github.com/werf/nelm/pkg/helm/pkg/werf/secrets" - -const DefaultSecretValuesFileName = internal.DefaultSecretValuesFileName diff --git a/pkg/featgate/feat.go b/pkg/featgate/feat.go index 036cf294..822b8a90 100644 --- a/pkg/featgate/feat.go +++ b/pkg/featgate/feat.go @@ -12,43 +12,11 @@ import ( var ( FeatGateEnvVarsPrefix = caps.ToScreamingSnake(common.Brand) + "_FEAT_" // Contains all defined feature gates. - FeatGates = []*FeatGate{} - FeatGateRemoteCharts = NewFeatGate( - "remote-charts", - `Allow not only local, but also remote charts as an argument to cli commands. Also adds the "--chart-version" option`, - ) - FeatGateNativeReleaseList = NewFeatGate( - "native-release-list", - `Use the native "release list" command instead of "helm list" exposed as "release list"`, - ) + FeatGates = []*FeatGate{} FeatGatePeriodicStackTraces = NewFeatGate( "periodic-stack-traces", `Print stack traces periodically to help with debugging deadlocks and other issues`, ) - FeatGateNativeReleaseUninstall = NewFeatGate( - "native-release-uninstall", - `Use the new "release uninstall" command implementation (not fully backwards compatible)`, - ) - FeatGateFieldSensitive = NewFeatGate( - "field-sensitive", - `Enable JSONPath-based selective sensitive field redaction`, - ) - FeatGatePreviewV2 = NewFeatGate( - "preview-v2", - `Activate all feature gates that will be enabled by default in Nelm v2`, - ) - FeatGateCleanNullFields = NewFeatGate( - "clean-null-fields", - `Enable cleaning of null fields from resource manifests for better Helm chart compatibility`, - ) - FeatGateMoreDetailedExitCodeForPlan = NewFeatGate( - "more-detailed-exit-code-for-plan", - `Make the "plan" command with the flag "--exit-code" return an exit code 3 instead of 2 when no resource changes, but still must install the release`, - ) - FeatGateResourceValidation = NewFeatGate( - "resource-validation", - "Validate chart resources against specific Kubernetes resources' schemas", - ) FeatGateTypescript = NewFeatGate( "typescript", `Enable TypeScript chart rendering from ts/ directory`, diff --git a/pkg/helm/cmd/helm/dependency.go b/pkg/helm/cmd/helm/dependency.go deleted file mode 100644 index 8b9b62b0..00000000 --- a/pkg/helm/cmd/helm/dependency.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "io" - "path/filepath" - - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/cmd/helm/require" - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -const dependencyDesc = ` -Manage the dependencies of a chart. - -Helm charts store their dependencies in 'charts/'. For chart developers, it is -often easier to manage dependencies in 'Chart.yaml' which declares all -dependencies. - -The dependency commands operate on that file, making it easy to synchronize -between the desired dependencies and the actual dependencies stored in the -'charts/' directory. - -For example, this Chart.yaml declares two dependencies: - - # Chart.yaml - dependencies: - - name: nginx - version: "1.2.3" - repository: "https://example.com/charts" - - name: memcached - version: "3.2.1" - repository: "https://another.example.com/charts" - - -The 'name' should be the name of a chart, where that name must match the name -in that chart's 'Chart.yaml' file. - -The 'version' field should contain a semantic version or version range. - -The 'repository' URL should point to a Chart Repository. Helm expects that by -appending '/index.yaml' to the URL, it should be able to retrieve the chart -repository's index. Note: 'repository' can be an alias. The alias must start -with 'alias:' or '@'. - -Starting from 2.2.0, repository can be defined as the path to the directory of -the dependency charts stored locally. The path should start with a prefix of -"file://". For example, - - # Chart.yaml - dependencies: - - name: nginx - version: "1.2.3" - repository: "file://../dependency_chart/nginx" - -If the dependency chart is retrieved locally, it is not required to have the -repository added to helm by "helm add repo". Version matching is also supported -for this case. -` - -const dependencyListDesc = ` -List all of the dependencies declared in a chart. - -This can take chart archives and chart directories as input. It will not alter -the contents of a chart. - -This will produce an error if the chart cannot be loaded. -` - -func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - cmd := &cobra.Command{ - Use: "dependency update|build|list", - Aliases: []string{"dep", "dependencies"}, - Short: "manage a chart's dependencies", - Long: dependencyDesc, - Args: require.NoArgs, - } - - cmd.AddCommand(newDependencyListCmd(out)) - cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) - cmd.AddCommand(newDependencyBuildCmd(cfg, out)) - - return cmd -} - -func newDependencyListCmd(out io.Writer) *cobra.Command { - client := action.NewDependency() - cmd := &cobra.Command{ - Use: "list CHART", - Aliases: []string{"ls"}, - Short: "list the dependencies for the given chart", - Long: dependencyListDesc, - Args: require.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - chartpath := "." - if len(args) > 0 { - chartpath = filepath.Clean(args[0]) - } - return client.List(chartpath, out, helmopts.HelmOptions{}) - }, - } - - f := cmd.Flags() - - f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table") - return cmd -} diff --git a/pkg/helm/cmd/helm/dependency_test.go b/pkg/helm/cmd/helm/dependency_test.go deleted file mode 100644 index b0b6f3b0..00000000 --- a/pkg/helm/cmd/helm/dependency_test.go +++ /dev/null @@ -1,57 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "runtime" - "testing" -) - -func TestDependencyListCmd(t *testing.T) { - noSuchChart := cmdTestCase{ - name: "No such chart", - cmd: "dependency list /no/such/chart", - golden: "output/dependency-list-no-chart-linux.txt", - wantError: true, - } - - noDependencies := cmdTestCase{ - name: "No dependencies", - cmd: "dependency list testdata/testcharts/alpine", - golden: "output/dependency-list-no-requirements-linux.txt", - } - - if runtime.GOOS == "windows" { - noSuchChart.golden = "output/dependency-list-no-chart-windows.txt" - noDependencies.golden = "output/dependency-list-no-requirements-windows.txt" - } - - tests := []cmdTestCase{noSuchChart, - noDependencies, { - name: "Dependencies in chart dir", - cmd: "dependency list testdata/testcharts/reqtest", - golden: "output/dependency-list.txt", - }, { - name: "Dependencies in chart archive", - cmd: "dependency list testdata/testcharts/reqtest-0.1.0.tgz", - golden: "output/dependency-list-archive.txt", - }} - runTestCmd(t, tests) -} - -func TestDependencyFileCompletion(t *testing.T) { - checkFileCompletion(t, "dependency", false) -} diff --git a/pkg/helm/cmd/helm/exports.go b/pkg/helm/cmd/helm/exports.go deleted file mode 100644 index b88f300b..00000000 --- a/pkg/helm/cmd/helm/exports.go +++ /dev/null @@ -1,36 +0,0 @@ -package helm - -var ( - Settings = settings - - LoadReleasesInMemory = loadReleasesInMemory - Debug = debug - NewRootCmd = newRootCmd - - NewDependencyCmd = newDependencyCmd - NewHistoryCmd = newHistoryCmd - NewListCmd = newListCmd - NewRepoCmd = newRepoCmd - NewPackageCmd = newPackageCmd - NewSearchCmd = newSearchCmd - NewRegistryCmd = newRegistryCmd - NewPullCmd = newPullCmd - NewPushCmd = newPushCmd - - LoadPlugins = loadPlugins -) - -func IsPluginError(err error) bool { - if err != nil { - _, isPluginErr := err.(pluginError) - return isPluginErr - } - return false -} - -func PluginErrorCode(err error) int { - if pluginErr, ok := err.(pluginError); ok { - return pluginErr.code - } - return 0 -} diff --git a/pkg/helm/cmd/helm/flags.go b/pkg/helm/cmd/helm/flags.go deleted file mode 100644 index c80e8f11..00000000 --- a/pkg/helm/cmd/helm/flags.go +++ /dev/null @@ -1,252 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "flag" - "fmt" - "log" - "path/filepath" - "sort" - "strings" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "k8s.io/klog/v2" - - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/cli/output" - "github.com/werf/nelm/pkg/helm/pkg/cli/values" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/postrender" - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -const ( - outputFlag = "output" - postRenderFlag = "post-renderer" - postRenderArgsFlag = "post-renderer-args" -) - -func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { - f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") - f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") - f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") - f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2)") - f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line") -} - -func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { - f.StringVar(&c.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used") - f.BoolVar(&c.Verify, "verify", false, "verify the package before using it") - f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification") - f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart") - f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart") - f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart") - f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") - f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") - f.BoolVar(&c.InsecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") - f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") - f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") -} - -// bindOutputFlag will add the output flag to the given command and bind the -// value to the given format pointer -func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { - cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o", - fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", "))) - - err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - var formatNames []string - for format, desc := range output.FormatsWithDesc() { - formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc)) - } - - // Sort the results to get a deterministic order for the tests - sort.Strings(formatNames) - return formatNames, cobra.ShellCompDirectiveNoFileComp - }) - - if err != nil { - log.Fatal(err) - } -} - -type outputValue output.Format - -func newOutputValue(defaultValue output.Format, p *output.Format) *outputValue { - *p = defaultValue - return (*outputValue)(p) -} - -func (o *outputValue) String() string { - // It is much cleaner looking (and technically less allocations) to just - // convert to a string rather than type asserting to the underlying - // output.Format - return string(*o) -} - -func (o *outputValue) Type() string { - return "format" -} - -func (o *outputValue) Set(s string) error { - outfmt, err := output.ParseFormat(s) - if err != nil { - return err - } - *o = outputValue(outfmt) - return nil -} - -func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) { - p := &postRendererOptions{varRef, "", []string{}} - cmd.Flags().Var(&postRendererString{p}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path") - cmd.Flags().Var(&postRendererArgsSlice{p}, postRenderArgsFlag, "an argument to the post-renderer (can specify multiple)") -} - -type postRendererOptions struct { - renderer *postrender.PostRenderer - binaryPath string - args []string -} - -type postRendererString struct { - options *postRendererOptions -} - -func (p *postRendererString) String() string { - return p.options.binaryPath -} - -func (p *postRendererString) Type() string { - return "postRendererString" -} - -func (p *postRendererString) Set(val string) error { - if val == "" { - return nil - } - p.options.binaryPath = val - pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...) - if err != nil { - return err - } - *p.options.renderer = pr - return nil -} - -type postRendererArgsSlice struct { - options *postRendererOptions -} - -func (p *postRendererArgsSlice) String() string { - return "[" + strings.Join(p.options.args, ",") + "]" -} - -func (p *postRendererArgsSlice) Type() string { - return "postRendererArgsSlice" -} - -func (p *postRendererArgsSlice) Set(val string) error { - - // a post-renderer defined by a user may accept empty arguments - p.options.args = append(p.options.args, val) - - if p.options.binaryPath == "" { - return nil - } - // overwrite if already create PostRenderer by `post-renderer` flags - pr, err := postrender.NewExec(p.options.binaryPath, p.options.args...) - if err != nil { - return err - } - *p.options.renderer = pr - return nil -} - -func (p *postRendererArgsSlice) Append(val string) error { - p.options.args = append(p.options.args, val) - return nil -} - -func (p *postRendererArgsSlice) Replace(val []string) error { - p.options.args = val - return nil -} - -func (p *postRendererArgsSlice) GetSlice() []string { - return p.options.args -} - -func compVersionFlag(chartRef string, _ string) ([]string, cobra.ShellCompDirective) { - chartInfo := strings.Split(chartRef, "/") - if len(chartInfo) != 2 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - - repoName := chartInfo[0] - chartName := chartInfo[1] - - path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) - - var versions []string - if indexFile, err := repo.LoadIndexFile(path); err == nil { - for _, details := range indexFile.Entries[chartName] { - appVersion := details.Metadata.AppVersion - appVersionDesc := "" - if appVersion != "" { - appVersionDesc = fmt.Sprintf("App: %s, ", appVersion) - } - created := details.Created.Format("January 2, 2006") - createdDesc := "" - if created != "" { - createdDesc = fmt.Sprintf("Created: %s ", created) - } - deprecated := "" - if details.Metadata.Deprecated { - deprecated = "(deprecated)" - } - versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated)) - } - } - - return versions, cobra.ShellCompDirectiveNoFileComp -} - -// addKlogFlags adds flags from k8s.io/klog -// marks the flags as hidden to avoid polluting the help text -func addKlogFlags(fs *pflag.FlagSet) { - local := flag.NewFlagSet("klog", flag.ExitOnError) - klog.InitFlags(local) - local.VisitAll(func(fl *flag.Flag) { - fl.Name = normalize(fl.Name) - if fs.Lookup(fl.Name) != nil { - return - } - newflag := pflag.PFlagFromGoFlag(fl) - newflag.Hidden = true - fs.AddFlag(newflag) - }) -} - -// normalize replaces underscores with hyphens -func normalize(s string) string { - return strings.ReplaceAll(s, "_", "-") -} diff --git a/pkg/helm/cmd/helm/flags_test.go b/pkg/helm/cmd/helm/flags_test.go deleted file mode 100644 index e5d653a1..00000000 --- a/pkg/helm/cmd/helm/flags_test.go +++ /dev/null @@ -1,95 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/release" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" -) - -func outputFlagCompletionTest(t *testing.T, cmdName string) { - releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { - info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() - return []*release.Release{{ - Name: "athos", - Namespace: "default", - Info: info, - Chart: &chart.Chart{}, - Hooks: hooks, - }, { - Name: "porthos", - Namespace: "default", - Info: info, - Chart: &chart.Chart{}, - Hooks: hooks, - }, { - Name: "aramis", - Namespace: "default", - Info: info, - Chart: &chart.Chart{}, - Hooks: hooks, - }, { - Name: "dartagnan", - Namespace: "gascony", - Info: info, - Chart: &chart.Chart{}, - Hooks: hooks, - }} - } - - tests := []cmdTestCase{{ - name: "completion for output flag long and before arg", - cmd: fmt.Sprintf("__complete %s --output ''", cmdName), - golden: "output/output-comp.txt", - rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, - }), - }, { - name: "completion for output flag long and after arg", - cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName), - golden: "output/output-comp.txt", - rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, - }), - }, { - name: "completion for output flag short and before arg", - cmd: fmt.Sprintf("__complete %s -o ''", cmdName), - golden: "output/output-comp.txt", - rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, - }), - }, { - name: "completion for output flag short and after arg", - cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName), - golden: "output/output-comp.txt", - rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, - }), - }, { - name: "completion for output flag, no filter", - cmd: fmt.Sprintf("__complete %s --output jso", cmdName), - golden: "output/output-comp.txt", - rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, - }), - }} - runTestCmd(t, tests) -} diff --git a/pkg/helm/cmd/helm/helm.go b/pkg/helm/cmd/helm/helm.go deleted file mode 100644 index dbfef0d7..00000000 --- a/pkg/helm/cmd/helm/helm.go +++ /dev/null @@ -1,170 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm // import "helm.sh/helm/v3/cmd/helm" - -import ( - "fmt" - "io" - "log" - "os" - "strings" - - "github.com/spf13/cobra" - "sigs.k8s.io/yaml" - - // Import to initialize client auth plugins. - _ "k8s.io/client-go/plugin/pkg/client/auth" - - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/cli" - "github.com/werf/nelm/pkg/helm/pkg/kube" - kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" -) - -var settings = cli.New() - -func init() { - log.SetFlags(log.Lshortfile) -} - -func debug(format string, v ...interface{}) { - if settings.Debug { - format = fmt.Sprintf("[debug] %s\n", format) - log.Output(2, fmt.Sprintf(format, v...)) - } -} - -func warning(format string, v ...interface{}) { - format = fmt.Sprintf("WARNING: %s\n", format) - fmt.Fprintf(os.Stderr, format, v...) -} - -func main() { - // Setting the name of the app for managedFields in the Kubernetes client. - // It is set here to the full name of "helm" so that renaming of helm to - // another name (e.g., helm2 or helm3) does not change the name of the - // manager as picked up by the automated name detection. - kube.ManagedFieldsManager = "helm" - - actionConfig := new(action.Configuration) - cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) - if err != nil { - warning("%+v", err) - os.Exit(1) - } - - // run when each command's execute method is called - cobra.OnInitialize(func() { - helmDriver := os.Getenv("HELM_DRIVER") - if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil { - log.Fatal(err) - } - if helmDriver == "memory" { - loadReleasesInMemory(actionConfig) - } - }) - - if err := cmd.Execute(); err != nil { - debug("%+v", err) - switch e := err.(type) { - case pluginError: - os.Exit(e.code) - default: - os.Exit(1) - } - } -} - -// This function loads releases into the memory storage if the -// environment variable is properly set. -func loadReleasesInMemory(actionConfig *action.Configuration) { - filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":") - if len(filePaths) == 0 { - return - } - - store := actionConfig.Releases - mem, ok := store.Driver.(*driver.Memory) - if !ok { - // For an unexpected reason we are not dealing with the memory storage driver. - return - } - - actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard} - - for _, path := range filePaths { - b, err := os.ReadFile(path) - if err != nil { - log.Fatal("Unable to read memory driver data", err) - } - - releases := []*release.Release{} - if err := yaml.Unmarshal(b, &releases); err != nil { - log.Fatal("Unable to unmarshal memory driver data: ", err) - } - - for _, rel := range releases { - if err := store.Create(rel); err != nil { - log.Fatal(err) - } - } - } - // Must reset namespace to the proper one - mem.SetNamespace(settings.Namespace()) -} - -func Init() (*cobra.Command, error) { - kube.ManagedFieldsManager = "helm" - - actionConfig := new(action.Configuration) - cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) - if err != nil { - return nil, err - } - - setCmdPreRun(cmd, actionConfig) - - return cmd, nil -} - -func setCmdPreRun(cmd *cobra.Command, actionConfig *action.Configuration) { - originalPersistentPreRunE := cmd.PreRunE - cmd.PreRunE = func(cmd *cobra.Command, args []string) error { - if originalPersistentPreRunE != nil { - if err := originalPersistentPreRunE(cmd, args); err != nil { - return err - } - } - - helmDriver := os.Getenv("HELM_DRIVER") - if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil { - return err - } - - if helmDriver == "memory" { - loadReleasesInMemory(actionConfig) - } - - return nil - } - - for _, cmd := range cmd.Commands() { - setCmdPreRun(cmd, actionConfig) - } -} diff --git a/pkg/helm/cmd/helm/helm_test.go b/pkg/helm/cmd/helm/helm_test.go deleted file mode 100644 index 6fcdd675..00000000 --- a/pkg/helm/cmd/helm/helm_test.go +++ /dev/null @@ -1,222 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "bytes" - "io" - "os" - "os/exec" - "runtime" - "strings" - "testing" - - shellwords "github.com/mattn/go-shellwords" - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/intern/test" - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/cli" - kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/storage" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - "github.com/werf/nelm/pkg/helm/pkg/time" -) - -func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } - -func init() { - action.Timestamper = testTimestamper -} - -func runTestCmd(t *testing.T, tests []cmdTestCase) { - t.Helper() - for _, tt := range tests { - for i := 0; i <= tt.repeat; i++ { - t.Run(tt.name, func(t *testing.T) { - defer resetEnv()() - - storage := storageFixture() - for _, rel := range tt.rels { - if err := storage.Create(rel); err != nil { - t.Fatal(err) - } - } - t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd) - _, out, err := executeActionCommandC(storage, tt.cmd) - if tt.wantError && err == nil { - t.Errorf("expected error, got success with the following output:\n%s", out) - } - if !tt.wantError && err != nil { - t.Errorf("expected no error, got: '%v'", err) - } - if tt.golden != "" { - test.AssertGoldenString(t, out, tt.golden) - } - }) - } - } -} - -func storageFixture() *storage.Storage { - return storage.Init(driver.NewMemory()) -} - -func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { - return executeActionCommandStdinC(store, nil, cmd) -} - -func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { - args, err := shellwords.Parse(cmd) - if err != nil { - return nil, "", err - } - - buf := new(bytes.Buffer) - - actionConfig := &action.Configuration{ - Releases: store, - KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, - Capabilities: chartutil.DefaultCapabilities, - Log: func(format string, v ...interface{}) {}, - } - - root, err := newRootCmd(actionConfig, buf, args) - if err != nil { - return nil, "", err - } - - root.SetOut(buf) - root.SetErr(buf) - root.SetArgs(args) - - oldStdin := os.Stdin - if in != nil { - root.SetIn(in) - os.Stdin = in - } - - if mem, ok := store.Driver.(*driver.Memory); ok { - mem.SetNamespace(settings.Namespace()) - } - c, err := root.ExecuteC() - - result := buf.String() - - os.Stdin = oldStdin - - return c, result, err -} - -// cmdTestCase describes a test case that works with releases. -type cmdTestCase struct { - name string - cmd string - golden string - wantError bool - // Rels are the available releases at the start of the test. - rels []*release.Release - // Number of repeats (in case a feature was previously flaky and the test checks - // it's now stably producing identical results). 0 means test is run exactly once. - repeat int -} - -func executeActionCommand(cmd string) (*cobra.Command, string, error) { - return executeActionCommandC(storageFixture(), cmd) -} - -func resetEnv() func() { - origEnv := os.Environ() - return func() { - os.Clearenv() - for _, pair := range origEnv { - kv := strings.SplitN(pair, "=", 2) - os.Setenv(kv[0], kv[1]) - } - settings = cli.New() - } -} - -func testChdir(t *testing.T, dir string) func() { - t.Helper() - old, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - if err := os.Chdir(dir); err != nil { - t.Fatal(err) - } - return func() { os.Chdir(old) } -} - -func TestPluginExitCode(t *testing.T) { - if os.Getenv("RUN_MAIN_FOR_TESTING") == "1" { - os.Args = []string{"helm", "exitwith", "2"} - - // We DO call helm's main() here. So this looks like a normal `helm` process. - main() - - // As main calls os.Exit, we never reach this line. - // But the test called this block of code catches and verifies the exit code. - return - } - - // Currently, plugins assume a Linux subsystem. Skip the execution - // tests until this is fixed - if runtime.GOOS != "windows" { - // Do a second run of this specific test(TestPluginExitCode) with RUN_MAIN_FOR_TESTING=1 set, - // So that the second run is able to run main() and this first run can verify the exit status returned by that. - // - // This technique originates from https://talks.golang.org/2014/testing.slide#23. - cmd := exec.Command(os.Args[0], "-test.run=TestPluginExitCode") - cmd.Env = append( - os.Environ(), - "RUN_MAIN_FOR_TESTING=1", - // See pkg/cli/environment.go for which envvars can be used for configuring these passes - // and also see plugin_test.go for how a plugin env can be set up. - // We just does the same setup as plugin_test.go via envvars - "HELM_PLUGINS=testdata/helmhome/helm/plugins", - "HELM_REPOSITORY_CONFIG=testdata/helmhome/helm/repositories.yaml", - "HELM_REPOSITORY_CACHE=testdata/helmhome/helm/repository", - ) - stdout := &bytes.Buffer{} - stderr := &bytes.Buffer{} - cmd.Stdout = stdout - cmd.Stderr = stderr - err := cmd.Run() - exiterr, ok := err.(*exec.ExitError) - - if !ok { - t.Fatalf("Unexpected error returned by os.Exit: %T", err) - } - - if stdout.String() != "" { - t.Errorf("Expected no write to stdout: Got %q", stdout.String()) - } - - expectedStderr := "Error: plugin \"exitwith\" exited with error\n" - if stderr.String() != expectedStderr { - t.Errorf("Expected %q written to stderr: Got %q", expectedStderr, stderr.String()) - } - - if exiterr.ExitCode() != 2 { - t.Errorf("Expected exit code 2: Got %d", exiterr.ExitCode()) - } - } -} diff --git a/pkg/helm/cmd/helm/history.go b/pkg/helm/cmd/helm/history.go deleted file mode 100644 index 5ae49598..00000000 --- a/pkg/helm/cmd/helm/history.go +++ /dev/null @@ -1,200 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "io" - "strconv" - "time" - - "github.com/gosuri/uitable" - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/cmd/helm/require" - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/cli/output" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" -) - -var historyHelp = ` -History prints historical revisions for a given release. - -A default maximum of 256 revisions will be returned. Setting '--max' -configures the maximum length of the revision list returned. - -The historical release set is printed as a formatted table, e.g: - - $ helm history angry-bird - REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION - 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install - 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully - 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2 - 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully -` - -func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - client := action.NewHistory(cfg) - var outfmt output.Format - - cmd := &cobra.Command{ - Use: "history RELEASE_NAME", - Long: historyHelp, - Short: "fetch release history", - Aliases: []string{"hist"}, - Args: require.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return compListReleases(toComplete, args, cfg) - }, - RunE: func(cmd *cobra.Command, args []string) error { - history, err := getHistory(client, args[0]) - if err != nil { - return err - } - - return outfmt.Write(out, history) - }, - } - - f := cmd.Flags() - f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") - bindOutputFlag(cmd, &outfmt) - - return cmd -} - -type releaseInfo struct { - Revision int `json:"revision"` - Updated helmtime.Time `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` - Description string `json:"description"` -} - -type releaseHistory []releaseInfo - -func (r releaseHistory) WriteJSON(out io.Writer) error { - return output.EncodeJSON(out, r) -} - -func (r releaseHistory) WriteYAML(out io.Writer) error { - return output.EncodeYAML(out, r) -} - -func (r releaseHistory) WriteTable(out io.Writer) error { - tbl := uitable.New() - tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") - for _, item := range r { - tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) - } - return output.EncodeTable(out, tbl) -} - -func getHistory(client *action.History, name string) (releaseHistory, error) { - hist, err := client.Run(name) - if err != nil { - return nil, err - } - - releaseutil.Reverse(hist, releaseutil.SortByRevision) - - var rels []*release.Release - for i := 0; i < min(len(hist), client.Max); i++ { - rels = append(rels, hist[i]) - } - - if len(rels) == 0 { - return releaseHistory{}, nil - } - - releaseHistory := getReleaseHistory(rels) - - return releaseHistory, nil -} - -func getReleaseHistory(rls []*release.Release) (history releaseHistory) { - for i := len(rls) - 1; i >= 0; i-- { - r := rls[i] - c := formatChartname(r.Chart) - s := r.Info.Status.String() - v := r.Version - d := r.Info.Description - a := formatAppVersion(r.Chart) - - rInfo := releaseInfo{ - Revision: v, - Status: s, - Chart: c, - AppVersion: a, - Description: d, - } - if !r.Info.LastDeployed.IsZero() { - rInfo.Updated = r.Info.LastDeployed - - } - history = append(history, rInfo) - } - - return history -} - -func formatChartname(c *chart.Chart) string { - if c == nil || c.Metadata == nil { - // This is an edge case that has happened in prod, though we don't - // know how: https://github.com/helm/helm/issues/1347 - return "MISSING" - } - return fmt.Sprintf("%s-%s", c.Name(), c.Metadata.Version) -} - -func formatAppVersion(c *chart.Chart) string { - if c == nil || c.Metadata == nil { - // This is an edge case that has happened in prod, though we don't - // know how: https://github.com/helm/helm/issues/1347 - return "MISSING" - } - return c.AppVersion() -} - -func min(x, y int) int { - if x < y { - return x - } - return y -} - -func compListRevisions(_ string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) { - client := action.NewHistory(cfg) - - var revisions []string - if hist, err := client.Run(releaseName); err == nil { - for _, release := range hist { - appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion) - chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version) - revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(release.Version), appVersion, chartDesc)) - } - return revisions, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveError -} diff --git a/pkg/helm/cmd/helm/history_test.go b/pkg/helm/cmd/helm/history_test.go deleted file mode 100644 index dbc2e0e9..00000000 --- a/pkg/helm/cmd/helm/history_test.go +++ /dev/null @@ -1,124 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/release" -) - -func TestHistoryCmd(t *testing.T) { - mk := func(name string, vers int, status release.Status) *release.Release { - return release.Mock(&release.MockReleaseOptions{ - Name: name, - Version: vers, - Status: status, - }) - } - - tests := []cmdTestCase{{ - name: "get history for release", - cmd: "history angry-bird", - rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), - mk("angry-bird", 2, release.StatusSuperseded), - mk("angry-bird", 1, release.StatusSuperseded), - }, - golden: "output/history.txt", - }, { - name: "get history with max limit set", - cmd: "history angry-bird --max 2", - rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), - }, - golden: "output/history-limit.txt", - }, { - name: "get history with yaml output format", - cmd: "history angry-bird --output yaml", - rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), - }, - golden: "output/history.yaml", - }, { - name: "get history with json output format", - cmd: "history angry-bird --output json", - rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), - }, - golden: "output/history.json", - }} - runTestCmd(t, tests) -} - -func TestHistoryOutputCompletion(t *testing.T) { - outputFlagCompletionTest(t, "history") -} - -func revisionFlagCompletionTest(t *testing.T, cmdName string) { - mk := func(name string, vers int, status release.Status) *release.Release { - return release.Mock(&release.MockReleaseOptions{ - Name: name, - Version: vers, - Status: status, - }) - } - - releases := []*release.Release{ - mk("musketeers", 11, release.StatusDeployed), - mk("musketeers", 10, release.StatusSuperseded), - mk("musketeers", 9, release.StatusSuperseded), - mk("musketeers", 8, release.StatusSuperseded), - } - - tests := []cmdTestCase{{ - name: "completion for revision flag", - cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), - rels: releases, - golden: "output/revision-comp.txt", - }, { - name: "completion for revision flag, no filter", - cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName), - rels: releases, - golden: "output/revision-comp.txt", - }, { - name: "completion for revision flag with too few args", - cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), - rels: releases, - golden: "output/revision-wrong-args-comp.txt", - }, { - name: "completion for revision flag with too many args", - cmd: fmt.Sprintf("__complete %s three musketeers --revision ''", cmdName), - rels: releases, - golden: "output/revision-wrong-args-comp.txt", - }} - runTestCmd(t, tests) -} - -func TestHistoryCompletion(t *testing.T) { - checkReleaseCompletion(t, "history", false) -} - -func TestHistoryFileCompletion(t *testing.T) { - checkFileCompletion(t, "history", false) - checkFileCompletion(t, "history myrelease", false) -} diff --git a/pkg/helm/cmd/helm/list.go b/pkg/helm/cmd/helm/list.go deleted file mode 100644 index 3b735ec3..00000000 --- a/pkg/helm/cmd/helm/list.go +++ /dev/null @@ -1,251 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "io" - "os" - "strconv" - - "github.com/gosuri/uitable" - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/cmd/helm/require" - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/cli/output" - "github.com/werf/nelm/pkg/helm/pkg/release" -) - -var listHelp = ` -This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). - -By default, it lists only releases that are deployed or failed. Flags like -'--uninstalled' and '--all' will alter this behavior. Such flags can be combined: -'--uninstalled --failed'. - -By default, items are sorted alphabetically. Use the '-d' flag to sort by -release date. - -If the --filter flag is provided, it will be treated as a filter. Filters are -regular expressions (Perl compatible) that are applied to the list of releases. -Only items that match the filter will be returned. - - $ helm list --filter 'ara[a-z]+' - NAME UPDATED CHART - maudlin-arachnid 2020-06-18 14:17:46.125134977 +0000 UTC alpine-0.1.0 - -If no results are found, 'helm list' will exit 0, but with no output (or in -the case of no '-q' flag, only headers). - -By default, up to 256 items may be returned. To limit this, use the '--max' flag. -Setting '--max' to 0 will not return all results. Rather, it will return the -server's default, which may be much higher than 256. Pairing the '--max' -flag with the '--offset' flag allows you to page through results. -` - -func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - client := action.NewList(cfg) - var outfmt output.Format - - cmd := &cobra.Command{ - Use: "list", - Short: "list releases", - Long: listHelp, - Aliases: []string{"ls"}, - Args: require.NoArgs, - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { - if client.AllNamespaces { - if err := cfg.Init(settings.RESTClientGetter(), "", os.Getenv("HELM_DRIVER"), debug); err != nil { - return err - } - } - client.SetStateMask() - - results, err := client.Run() - if err != nil { - return err - } - - if client.Short { - names := make([]string, 0, len(results)) - for _, res := range results { - names = append(names, res.Name) - } - - outputFlag := cmd.Flag("output") - - switch outputFlag.Value.String() { - case "json": - output.EncodeJSON(out, names) - return nil - case "yaml": - output.EncodeYAML(out, names) - return nil - case "table": - for _, res := range results { - fmt.Fprintln(out, res.Name) - } - return nil - } - } - - return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders)) - }, - } - - f := cmd.Flags() - f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format") - f.BoolVarP(&client.NoHeaders, "no-headers", "", false, "don't print headers when using the default output format") - f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`) - f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") - f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") - f.BoolVarP(&client.All, "all", "a", false, "show all releases without any filter applied") - f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)") - f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") - f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") - f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") - f.BoolVar(&client.Failed, "failed", false, "show failed releases") - f.BoolVar(&client.Pending, "pending", false, "show pending releases") - f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces") - f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") - f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value") - f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results") - f.StringVarP(&client.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Works only for secret(default) and configmap storage backends.") - bindOutputFlag(cmd, &outfmt) - - return cmd -} - -type releaseElement struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - Revision string `json:"revision"` - Updated string `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` -} - -type releaseListWriter struct { - releases []releaseElement - noHeaders bool -} - -func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool) *releaseListWriter { - // Initialize the array so no results returns an empty array instead of null - elements := make([]releaseElement, 0, len(releases)) - for _, r := range releases { - element := releaseElement{ - Name: r.Name, - Namespace: r.Namespace, - Revision: strconv.Itoa(r.Version), - Status: r.Info.Status.String(), - Chart: formatChartname(r.Chart), - AppVersion: formatAppVersion(r.Chart), - } - - t := "-" - if tspb := r.Info.LastDeployed; !tspb.IsZero() { - if timeFormat != "" { - t = tspb.Format(timeFormat) - } else { - t = tspb.String() - } - } - element.Updated = t - - elements = append(elements, element) - } - return &releaseListWriter{elements, noHeaders} -} - -func (r *releaseListWriter) WriteTable(out io.Writer) error { - table := uitable.New() - if !r.noHeaders { - table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION") - } - for _, r := range r.releases { - table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion) - } - return output.EncodeTable(out, table) -} - -func (r *releaseListWriter) WriteJSON(out io.Writer) error { - return output.EncodeJSON(out, r.releases) -} - -func (r *releaseListWriter) WriteYAML(out io.Writer) error { - return output.EncodeYAML(out, r.releases) -} - -// Returns all releases from 'releases', except those with names matching 'ignoredReleases' -func filterReleases(releases []*release.Release, ignoredReleaseNames []string) []*release.Release { - // if ignoredReleaseNames is nil, just return releases - if ignoredReleaseNames == nil { - return releases - } - - var filteredReleases []*release.Release - for _, rel := range releases { - found := false - for _, ignoredName := range ignoredReleaseNames { - if rel.Name == ignoredName { - found = true - break - } - } - if !found { - filteredReleases = append(filteredReleases, rel) - } - } - - return filteredReleases -} - -// Provide dynamic auto-completion for release names -func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) { - cobra.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete), settings.Debug) - - client := action.NewList(cfg) - client.All = true - client.Limit = 0 - // Do not filter so as to get the entire list of releases. - // This will allow zsh and fish to match completion choices - // on other criteria then prefix. For example: - // helm status ingress - // can match - // helm status nginx-ingress - // - // client.Filter = fmt.Sprintf("^%s", toComplete) - - client.SetStateMask() - releases, err := client.Run() - if err != nil { - return nil, cobra.ShellCompDirectiveDefault - } - - var choices []string - filteredReleases := filterReleases(releases, ignoredReleaseNames) - for _, rel := range filteredReleases { - choices = append(choices, - fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String())) - } - - return choices, cobra.ShellCompDirectiveNoFileComp -} diff --git a/pkg/helm/cmd/helm/list_test.go b/pkg/helm/cmd/helm/list_test.go deleted file mode 100644 index 573b9092..00000000 --- a/pkg/helm/cmd/helm/list_test.go +++ /dev/null @@ -1,246 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/time" -) - -func TestListCmd(t *testing.T) { - defaultNamespace := "default" - - sampleTimeSeconds := int64(1452902400) - timestamp1 := time.Unix(sampleTimeSeconds+1, 0).UTC() - timestamp2 := time.Unix(sampleTimeSeconds+2, 0).UTC() - timestamp3 := time.Unix(sampleTimeSeconds+3, 0).UTC() - timestamp4 := time.Unix(sampleTimeSeconds+4, 0).UTC() - chartInfo := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chickadee", - Version: "1.0.0", - AppVersion: "0.0.1", - }, - } - - releaseFixture := []*release.Release{ - { - Name: "starlord", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusSuperseded, - }, - Chart: chartInfo, - }, - { - Name: "starlord", - Version: 2, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusDeployed, - }, - Chart: chartInfo, - }, - { - Name: "groot", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusUninstalled, - }, - Chart: chartInfo, - }, - { - Name: "gamora", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusSuperseded, - }, - Chart: chartInfo, - }, - { - Name: "rocket", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp2, - Status: release.StatusFailed, - }, - Chart: chartInfo, - }, - { - Name: "drax", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusUninstalling, - }, - Chart: chartInfo, - }, - { - Name: "thanos", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusPendingInstall, - }, - Chart: chartInfo, - }, - { - Name: "hummingbird", - Version: 1, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp3, - Status: release.StatusDeployed, - }, - Chart: chartInfo, - }, - { - Name: "iguana", - Version: 2, - Namespace: defaultNamespace, - Info: &release.Info{ - LastDeployed: timestamp4, - Status: release.StatusDeployed, - }, - Chart: chartInfo, - }, - { - Name: "starlord", - Version: 2, - Namespace: "milano", - Info: &release.Info{ - LastDeployed: timestamp1, - Status: release.StatusDeployed, - }, - Chart: chartInfo, - }, - } - - tests := []cmdTestCase{{ - name: "list releases", - cmd: "list", - golden: "output/list.txt", - rels: releaseFixture, - }, { - name: "list without headers", - cmd: "list --no-headers", - golden: "output/list-no-headers.txt", - rels: releaseFixture, - }, { - name: "list all releases", - cmd: "list --all", - golden: "output/list-all.txt", - rels: releaseFixture, - }, { - name: "list releases sorted by release date", - cmd: "list --date", - golden: "output/list-date.txt", - rels: releaseFixture, - }, { - name: "list failed releases", - cmd: "list --failed", - golden: "output/list-failed.txt", - rels: releaseFixture, - }, { - name: "list filtered releases", - cmd: "list --filter='.*'", - golden: "output/list-filter.txt", - rels: releaseFixture, - }, { - name: "list releases, limited to one release", - cmd: "list --max 1", - golden: "output/list-max.txt", - rels: releaseFixture, - }, { - name: "list releases, offset by one", - cmd: "list --offset 1", - golden: "output/list-offset.txt", - rels: releaseFixture, - }, { - name: "list pending releases", - cmd: "list --pending", - golden: "output/list-pending.txt", - rels: releaseFixture, - }, { - name: "list releases in reverse order", - cmd: "list --reverse", - golden: "output/list-reverse.txt", - rels: releaseFixture, - }, { - name: "list releases sorted by reversed release date", - cmd: "list --date --reverse", - golden: "output/list-date-reversed.txt", - rels: releaseFixture, - }, { - name: "list releases in short output format", - cmd: "list --short", - golden: "output/list-short.txt", - rels: releaseFixture, - }, { - name: "list releases in short output format", - cmd: "list --short --output yaml", - golden: "output/list-short-yaml.txt", - rels: releaseFixture, - }, { - name: "list releases in short output format", - cmd: "list --short --output json", - golden: "output/list-short-json.txt", - rels: releaseFixture, - }, { - name: "list superseded releases", - cmd: "list --superseded", - golden: "output/list-superseded.txt", - rels: releaseFixture, - }, { - name: "list uninstalled releases", - cmd: "list --uninstalled", - golden: "output/list-uninstalled.txt", - rels: releaseFixture, - }, { - name: "list releases currently uninstalling", - cmd: "list --uninstalling", - golden: "output/list-uninstalling.txt", - rels: releaseFixture, - }, { - name: "list releases in another namespace", - cmd: "list -n milano", - golden: "output/list-namespace.txt", - rels: releaseFixture, - }} - runTestCmd(t, tests) -} - -func TestListOutputCompletion(t *testing.T) { - outputFlagCompletionTest(t, "list") -} - -func TestListFileCompletion(t *testing.T) { - checkFileCompletion(t, "list", false) -} diff --git a/pkg/helm/cmd/helm/load_plugins.go b/pkg/helm/cmd/helm/load_plugins.go deleted file mode 100644 index a2e703c3..00000000 --- a/pkg/helm/cmd/helm/load_plugins.go +++ /dev/null @@ -1,377 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "bytes" - "fmt" - "io" - "log" - "os" - "os/exec" - "path/filepath" - "strconv" - "strings" - "syscall" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/plugin" -) - -const ( - pluginStaticCompletionFile = "completion.yaml" - pluginDynamicCompletionExecutable = "plugin.complete" -) - -type pluginError struct { - error - code int -} - -// loadPlugins loads plugins into the command list. -// -// This follows a different pattern than the other commands because it has -// to inspect its environment and then add commands to the base command -// as it finds them. -func loadPlugins(baseCmd *cobra.Command, out io.Writer) { - - // If HELM_NO_PLUGINS is set to 1, do not load plugins. - if os.Getenv("HELM_NO_PLUGINS") == "1" { - return - } - - found, err := plugin.FindPlugins(settings.PluginsDirectory) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err) - return - } - - // Now we create commands for all of these. - for _, plug := range found { - plug := plug - md := plug.Metadata - if md.Usage == "" { - md.Usage = fmt.Sprintf("the %q plugin", md.Name) - } - - c := &cobra.Command{ - Use: md.Name, - Short: md.Usage, - Long: md.Description, - RunE: func(cmd *cobra.Command, args []string) error { - u, err := processParent(cmd, args) - if err != nil { - return err - } - - // Call setupEnv before PrepareCommand because - // PrepareCommand uses os.ExpandEnv and expects the - // setupEnv vars. - plugin.SetupPluginEnv(settings, md.Name, plug.Dir) - main, argv, prepCmdErr := plug.PrepareCommand(u) - if prepCmdErr != nil { - os.Stderr.WriteString(prepCmdErr.Error()) - return errors.Errorf("plugin %q exited with error", md.Name) - } - - return callPluginExecutable(md.Name, main, argv, out) - }, - // This passes all the flags to the subcommand. - DisableFlagParsing: true, - } - - // TODO: Make sure a command with this name does not already exist. - baseCmd.AddCommand(c) - - // For completion, we try to load more details about the plugins so as to allow for command and - // flag completion of the plugin itself. - // We only do this when necessary (for the "completion" and "__complete" commands) to avoid the - // risk of a rogue plugin affecting Helm's normal behavior. - subCmd, _, err := baseCmd.Find(os.Args[1:]) - if (err == nil && - ((subCmd.HasParent() && subCmd.Parent().Name() == "completion") || subCmd.Name() == cobra.ShellCompRequestCmd)) || - /* for the tests */ subCmd == baseCmd.Root() { - loadCompletionForPlugin(c, plug) - } - } -} - -func processParent(cmd *cobra.Command, args []string) ([]string, error) { - k, u := manuallyProcessArgs(args) - if err := cmd.Parent().ParseFlags(k); err != nil { - return nil, err - } - return u, nil -} - -// This function is used to setup the environment for the plugin and then -// call the executable specified by the parameter 'main' -func callPluginExecutable(pluginName string, main string, argv []string, out io.Writer) error { - env := os.Environ() - for k, v := range settings.EnvVars() { - env = append(env, fmt.Sprintf("%s=%s", k, v)) - } - - mainCmdExp := os.ExpandEnv(main) - prog := exec.Command(mainCmdExp, argv...) - prog.Env = env - prog.Stdin = os.Stdin - prog.Stdout = out - prog.Stderr = os.Stderr - if err := prog.Run(); err != nil { - if eerr, ok := err.(*exec.ExitError); ok { - os.Stderr.Write(eerr.Stderr) - status := eerr.Sys().(syscall.WaitStatus) - return pluginError{ - error: errors.Errorf("plugin %q exited with error", pluginName), - code: status.ExitStatus(), - } - } - return err - } - return nil -} - -// manuallyProcessArgs processes an arg array, removing special args. -// -// Returns two sets of args: known and unknown (in that order) -func manuallyProcessArgs(args []string) ([]string, []string) { - known := []string{} - unknown := []string{} - kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--kube-ca-file", "--registry-config", "--repository-cache", "--repository-config", "--insecure-skip-tls-verify", "--tls-server-name"} - knownArg := func(a string) bool { - for _, pre := range kvargs { - if strings.HasPrefix(a, pre+"=") { - return true - } - } - return false - } - - isKnown := func(v string) string { - for _, i := range kvargs { - if i == v { - return v - } - } - return "" - } - - for i := 0; i < len(args); i++ { - switch a := args[i]; a { - case "--debug": - known = append(known, a) - case isKnown(a): - known = append(known, a) - i++ - if i < len(args) { - known = append(known, args[i]) - } - default: - if knownArg(a) { - known = append(known, a) - continue - } - unknown = append(unknown, a) - } - } - return known, unknown -} - -// pluginCommand represents the optional completion.yaml file of a plugin -type pluginCommand struct { - Name string `json:"name"` - ValidArgs []string `json:"validArgs"` - Flags []string `json:"flags"` - Commands []pluginCommand `json:"commands"` -} - -// loadCompletionForPlugin will load and parse any completion.yaml provided by the plugin -// and add the dynamic completion hook to call the optional plugin.complete -func loadCompletionForPlugin(pluginCmd *cobra.Command, plugin *plugin.Plugin) { - // Parse the yaml file providing the plugin's sub-commands and flags - cmds, err := loadFile(strings.Join( - []string{plugin.Dir, pluginStaticCompletionFile}, string(filepath.Separator))) - - if err != nil { - // The file could be missing or invalid. No static completion for this plugin. - if settings.Debug { - log.Output(2, fmt.Sprintf("[info] %s\n", err.Error())) - } - // Continue to setup dynamic completion. - cmds = &pluginCommand{} - } - - // Preserve the Usage string specified for the plugin - cmds.Name = pluginCmd.Use - - addPluginCommands(plugin, pluginCmd, cmds) -} - -// addPluginCommands is a recursive method that adds each different level -// of sub-commands and flags for the plugins that have provided such information -func addPluginCommands(plugin *plugin.Plugin, baseCmd *cobra.Command, cmds *pluginCommand) { - if cmds == nil { - return - } - - if len(cmds.Name) == 0 { - // Missing name for a command - if settings.Debug { - log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath())) - } - return - } - - baseCmd.Use = cmds.Name - baseCmd.ValidArgs = cmds.ValidArgs - // Setup the same dynamic completion for each plugin sub-command. - // This is because if dynamic completion is triggered, there is a single executable - // to call (plugin.complete), so every sub-commands calls it in the same fashion. - if cmds.Commands == nil { - // Only setup dynamic completion if there are no sub-commands. This avoids - // calling plugin.complete at every completion, which greatly simplifies - // development of plugin.complete for plugin developers. - baseCmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return pluginDynamicComp(plugin, cmd, args, toComplete) - } - } - - // Create fake flags. - if len(cmds.Flags) > 0 { - // The flags can be created with any type, since we only need them for completion. - // pflag does not allow to create short flags without a corresponding long form - // so we look for all short flags and match them to any long flag. This will allow - // plugins to provide short flags without a long form. - // If there are more short-flags than long ones, we'll create an extra long flag with - // the same single letter as the short form. - shorts := []string{} - longs := []string{} - for _, flag := range cmds.Flags { - if len(flag) == 1 { - shorts = append(shorts, flag) - } else { - longs = append(longs, flag) - } - } - - f := baseCmd.Flags() - if len(longs) >= len(shorts) { - for i := range longs { - if i < len(shorts) { - f.BoolP(longs[i], shorts[i], false, "") - } else { - f.Bool(longs[i], false, "") - } - } - } else { - for i := range shorts { - if i < len(longs) { - f.BoolP(longs[i], shorts[i], false, "") - } else { - // Create a long flag with the same name as the short flag. - // Not a perfect solution, but its better than ignoring the extra short flags. - f.BoolP(shorts[i], shorts[i], false, "") - } - } - } - } - - // Recursively add any sub-commands - for _, cmd := range cmds.Commands { - // Create a fake command so that completion can be done for the sub-commands of the plugin - subCmd := &cobra.Command{ - // This prevents Cobra from removing the flags. We want to keep the flags to pass them - // to the dynamic completion script of the plugin. - DisableFlagParsing: true, - // A Run is required for it to be a valid command without subcommands - Run: func(cmd *cobra.Command, args []string) {}, - } - baseCmd.AddCommand(subCmd) - addPluginCommands(plugin, subCmd, &cmd) - } -} - -// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object -func loadFile(path string) (*pluginCommand, error) { - cmds := new(pluginCommand) - b, err := os.ReadFile(path) - if err != nil { - return cmds, fmt.Errorf("file (%s) not provided by plugin. No plugin auto-completion possible", path) - } - - err = yaml.Unmarshal(b, cmds) - return cmds, err -} - -// pluginDynamicComp call the plugin.complete script of the plugin (if available) -// to obtain the dynamic completion choices. It must pass all the flags and sub-commands -// specified in the command-line to the plugin.complete executable (except helm's global flags) -func pluginDynamicComp(plug *plugin.Plugin, cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - md := plug.Metadata - - u, err := processParent(cmd, args) - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - // We will call the dynamic completion script of the plugin - main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator)) - - // We must include all sub-commands passed on the command-line. - // To do that, we pass-in the entire CommandPath, except the first two elements - // which are 'helm' and 'pluginName'. - argv := strings.Split(cmd.CommandPath(), " ")[2:] - if !md.IgnoreFlags { - argv = append(argv, u...) - argv = append(argv, toComplete) - } - plugin.SetupPluginEnv(settings, md.Name, plug.Dir) - - cobra.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv), settings.Debug) - buf := new(bytes.Buffer) - if err := callPluginExecutable(md.Name, main, argv, buf); err != nil { - // The dynamic completion file is optional for a plugin, so this error is ok. - cobra.CompDebugln(fmt.Sprintf("Unable to call %s: %v", main, err.Error()), settings.Debug) - return nil, cobra.ShellCompDirectiveDefault - } - - var completions []string - for _, comp := range strings.Split(buf.String(), "\n") { - // Remove any empty lines - if len(comp) > 0 { - completions = append(completions, comp) - } - } - - // Check if the last line of output is of the form :, which - // indicates the BashCompletionDirective. - directive := cobra.ShellCompDirectiveDefault - if len(completions) > 0 { - lastLine := completions[len(completions)-1] - if len(lastLine) > 1 && lastLine[0] == ':' { - if strInt, err := strconv.Atoi(lastLine[1:]); err == nil { - directive = cobra.ShellCompDirective(strInt) - completions = completions[:len(completions)-1] - } - } - } - - return completions, directive -} diff --git a/pkg/helm/cmd/helm/package.go b/pkg/helm/cmd/helm/package.go deleted file mode 100644 index e6c89bc0..00000000 --- a/pkg/helm/cmd/helm/package.go +++ /dev/null @@ -1,141 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "io" - "os" - "path/filepath" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/cli/values" - "github.com/werf/nelm/pkg/helm/pkg/downloader" - "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" - "github.com/werf/nelm/pkg/ts" -) - -const packageDesc = ` -This command packages a chart into a versioned chart archive file. If a path -is given, this will look at that path for a chart (which must contain a -Chart.yaml file) and then package that directory. - -Versioned chart archives are used by Helm package repositories. - -To sign a chart, use the '--sign' flag. In most cases, you should also -provide '--keyring path/to/secret/keys' and '--key keyname'. - - $ helm package --sign ./mychart --key mykey --keyring ~/.gnupg/secring.gpg - -If '--keyring' is not specified, Helm usually defaults to the public keyring -unless your environment is otherwise configured. -` - -func newPackageCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - client := action.NewPackage() - valueOpts := &values.Options{} - - cmd := &cobra.Command{ - Use: "package [CHART_PATH] [...]", - Short: "package a chart directory into a chart archive", - Long: packageDesc, - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) == 0 { - return errors.Errorf("need at least one argument, the path to the chart") - } - if client.Sign { - if client.Key == "" { - return errors.New("--key is required for signing a package") - } - if client.Keyring == "" { - return errors.New("--keyring is required for signing a package") - } - } - - tsOpts := ts.GetTSOptionsFromContext(cmd.Context()) - - opts := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ - NoSecrets: true, - }, - TypeScriptOpts: helmopts.TypeScriptOptions{ - DenoBinaryPath: tsOpts.DenoBinaryPath, - }, - } - - client.RepositoryConfig = settings.RepositoryConfig - client.RepositoryCache = settings.RepositoryCache - p := getter.All(settings) - vals, err := valueOpts.MergeValues(p, opts) - if err != nil { - return err - } - - for i := 0; i < len(args); i++ { - path, err := filepath.Abs(args[i]) - if err != nil { - return err - } - if _, err := os.Stat(args[i]); err != nil { - return err - } - - if client.DependencyUpdate { - downloadManager := &downloader.Manager{ - Out: io.Discard, - ChartPath: path, - Keyring: client.Keyring, - Getters: p, - Debug: settings.Debug, - RegistryClient: cfg.RegistryClient, - RepositoryConfig: settings.RepositoryConfig, - RepositoryCache: settings.RepositoryCache, - } - - opts.ChartLoadOpts.DepDownloader = downloadManager - - if err := downloadManager.Update(opts); err != nil { - return err - } - } - - p, err := client.Run(path, vals, opts) - if err != nil { - return err - } - fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p) - } - return nil - }, - } - - f := cmd.Flags() - f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package") - f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true") - f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring") - f.StringVar(&client.PassphraseFile, "passphrase-file", "", `location of a file which contains the passphrase for the signing key. Use "-" in order to read from stdin.`) - f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version") - f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") - f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") - f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) - - return cmd -} diff --git a/pkg/helm/cmd/helm/pull_test.go b/pkg/helm/cmd/helm/pull_test.go deleted file mode 100644 index b3087740..00000000 --- a/pkg/helm/cmd/helm/pull_test.go +++ /dev/null @@ -1,396 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" -) - -func TestPullCmd(t *testing.T) { - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") - if err != nil { - t.Fatal(err) - } - defer srv.Stop() - - ociSrv, err := repotest.NewOCIServer(t, srv.Root()) - if err != nil { - t.Fatal(err) - } - ociSrv.Run(t) - - if err := srv.LinkIndices(); err != nil { - t.Fatal(err) - } - - helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + - "Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" + - "Chart Hash Verified: " - - // all flags will get "-d outdir" appended. - tests := []struct { - name string - args string - existFile string - existDir string - wantError bool - wantErrorMsg string - failExpect string - expectFile string - expectDir bool - expectVerify bool - expectSha string - }{ - { - name: "Basic chart fetch", - args: "test/signtest", - expectFile: "./signtest-0.1.0.tgz", - }, - { - name: "Chart fetch with version", - args: "test/signtest --version=0.1.0", - expectFile: "./signtest-0.1.0.tgz", - }, - { - name: "Fail chart fetch with non-existent version", - args: "test/signtest --version=99.1.0", - wantError: true, - failExpect: "no such chart", - }, - { - name: "Fail fetching non-existent chart", - args: "test/nosuchthing", - failExpect: "Failed to fetch", - wantError: true, - }, - { - name: "Fetch and verify", - args: "test/signtest --verify --keyring testdata/helm-test-key.pub", - expectFile: "./signtest-0.1.0.tgz", - expectVerify: true, - expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", - }, - { - name: "Fetch and fail verify", - args: "test/reqtest --verify --keyring testdata/helm-test-key.pub", - failExpect: "Failed to fetch provenance", - wantError: true, - }, - { - name: "Fetch and untar", - args: "test/signtest --untar --untardir signtest", - expectFile: "./signtest", - expectDir: true, - }, - { - name: "Fetch untar when file with same name existed", - args: "test/test1 --untar --untardir test1", - existFile: "test1", - wantError: true, - wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test1")), - }, - { - name: "Fetch untar when dir with same name existed", - args: "test/test2 --untar --untardir test2", - existDir: "test2", - wantError: true, - wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test2")), - }, - { - name: "Fetch, verify, untar", - args: "test/signtest --verify --keyring=testdata/helm-test-key.pub --untar --untardir signtest2", - expectFile: "./signtest2", - expectDir: true, - expectVerify: true, - expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", - }, - { - name: "Chart fetch using repo URL", - expectFile: "./signtest-0.1.0.tgz", - args: "signtest --repo " + srv.URL(), - }, - { - name: "Fail fetching non-existent chart on repo URL", - args: "someChart --repo " + srv.URL(), - failExpect: "Failed to fetch chart", - wantError: true, - }, - { - name: "Specific version chart fetch using repo URL", - expectFile: "./signtest-0.1.0.tgz", - args: "signtest --version=0.1.0 --repo " + srv.URL(), - }, - { - name: "Specific version chart fetch using repo URL", - args: "signtest --version=0.2.0 --repo " + srv.URL(), - failExpect: "Failed to fetch chart version", - wantError: true, - }, - { - name: "Fetch OCI Chart", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0", ociSrv.RegistryURL), - expectFile: "./oci-dependent-chart-0.1.0.tgz", - }, - { - name: "Fetch OCI Chart with untar", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar", ociSrv.RegistryURL), - expectFile: "./oci-dependent-chart", - expectDir: true, - }, - { - name: "Fetch OCI Chart with untar and untardir", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2", ociSrv.RegistryURL), - expectFile: "./ocitest2", - expectDir: true, - }, - { - name: "OCI Fetch untar when dir with same name existed", - args: fmt.Sprintf("oci-test-chart oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2 --untar --untardir ocitest2", ociSrv.RegistryURL), - wantError: true, - wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "ocitest2")), - }, - { - name: "Fail fetching non-existent OCI chart", - args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing --version 0.1.0", ociSrv.RegistryURL), - failExpect: "Failed to fetch", - wantError: true, - }, - { - name: "Fail fetching OCI chart without version specified", - args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing", ociSrv.RegistryURL), - wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", - wantError: true, - }, - { - name: "Fail fetching OCI chart without version specified", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL), - wantErrorMsg: "Error: --version flag is explicitly required for OCI registries", - wantError: true, - }, - { - name: "Fail fetching OCI chart without version specified", - args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL), - wantError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - outdir := srv.Root() - cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s", - tt.args, - outdir, - filepath.Join(outdir, "repositories.yaml"), - outdir, - filepath.Join(outdir, "config.json"), - ) - // Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182 - if tt.existFile != "" { - file := filepath.Join(outdir, tt.existFile) - _, err := os.Create(file) - if err != nil { - t.Fatal(err) - } - } - if tt.existDir != "" { - file := filepath.Join(outdir, tt.existDir) - err := os.Mkdir(file, 0755) - if err != nil { - t.Fatal(err) - } - } - _, out, err := executeActionCommand(cmd) - if err != nil { - if tt.wantError { - if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() { - t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg) - } - return - } - t.Fatalf("%q reported error: %s", tt.name, err) - } - - if tt.expectVerify { - outString := helmTestKeyOut + tt.expectSha + "\n" - if out != outString { - t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out) - } - - } - - ef := filepath.Join(outdir, tt.expectFile) - fi, err := os.Stat(ef) - if err != nil { - t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) - } - if fi.IsDir() != tt.expectDir { - t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) - } - }) - } -} - -func TestPullWithCredentialsCmd(t *testing.T) { - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*") - if err != nil { - t.Fatal(err) - } - defer srv.Stop() - - srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - if !ok || username != "username" || password != "password" { - t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) - } - })) - - srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r) - })) - defer srv2.Close() - - if err := srv.LinkIndices(); err != nil { - t.Fatal(err) - } - - // all flags will get "-d outdir" appended. - tests := []struct { - name string - args string - existFile string - existDir string - wantError bool - wantErrorMsg string - expectFile string - expectDir bool - }{ - { - name: "Chart fetch using repo URL", - expectFile: "./signtest-0.1.0.tgz", - args: "signtest --repo " + srv.URL() + " --username username --password password", - }, - { - name: "Fail fetching non-existent chart on repo URL", - args: "someChart --repo " + srv.URL() + " --username username --password password", - wantError: true, - }, - { - name: "Specific version chart fetch using repo URL", - expectFile: "./signtest-0.1.0.tgz", - args: "signtest --version=0.1.0 --repo " + srv.URL() + " --username username --password password", - }, - { - name: "Specific version chart fetch using repo URL", - args: "signtest --version=0.2.0 --repo " + srv.URL() + " --username username --password password", - wantError: true, - }, - { - name: "Chart located on different domain with credentials passed", - args: "reqtest --repo " + srv2.URL + " --username username --password password --pass-credentials", - expectFile: "./reqtest-0.1.0.tgz", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - outdir := srv.Root() - cmd := fmt.Sprintf("pull %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s", - tt.args, - outdir, - filepath.Join(outdir, "repositories.yaml"), - outdir, - filepath.Join(outdir, "config.json"), - ) - // Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182 - if tt.existFile != "" { - file := filepath.Join(outdir, tt.existFile) - _, err := os.Create(file) - if err != nil { - t.Fatal(err) - } - } - if tt.existDir != "" { - file := filepath.Join(outdir, tt.existDir) - err := os.Mkdir(file, 0755) - if err != nil { - t.Fatal(err) - } - } - _, _, err := executeActionCommand(cmd) - if err != nil { - if tt.wantError { - if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() { - t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg) - } - return - } - t.Fatalf("%q reported error: %s", tt.name, err) - } - - ef := filepath.Join(outdir, tt.expectFile) - fi, err := os.Stat(ef) - if err != nil { - t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) - } - if fi.IsDir() != tt.expectDir { - t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) - } - }) - } -} - -func TestPullVersionCompletion(t *testing.T) { - repoFile := "testdata/helmhome/helm/repositories.yaml" - repoCache := "testdata/helmhome/helm/repository" - - repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) - - tests := []cmdTestCase{{ - name: "completion for pull version flag", - cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup), - golden: "output/version-comp.txt", - }, { - name: "completion for pull version flag, no filter", - cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup), - golden: "output/version-comp.txt", - }, { - name: "completion for pull version flag too few args", - cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup), - golden: "output/version-invalid-comp.txt", - }, { - name: "completion for pull version flag too many args", - cmd: fmt.Sprintf("%s __complete pull testing/alpine badarg --version ''", repoSetup), - golden: "output/version-invalid-comp.txt", - }, { - name: "completion for pull version flag invalid chart", - cmd: fmt.Sprintf("%s __complete pull invalid/invalid --version ''", repoSetup), - golden: "output/version-invalid-comp.txt", - }} - runTestCmd(t, tests) -} - -func TestPullFileCompletion(t *testing.T) { - checkFileCompletion(t, "pull", false) - checkFileCompletion(t, "pull repo/chart", false) -} diff --git a/pkg/helm/cmd/helm/push_test.go b/pkg/helm/cmd/helm/push_test.go deleted file mode 100644 index 896ff36b..00000000 --- a/pkg/helm/cmd/helm/push_test.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "testing" -) - -func TestPushFileCompletion(t *testing.T) { - checkFileCompletion(t, "push", true) - checkFileCompletion(t, "push package.tgz", false) - checkFileCompletion(t, "push package.tgz oci://localhost:5000", false) -} diff --git a/pkg/helm/cmd/helm/registry.go b/pkg/helm/cmd/helm/registry.go deleted file mode 100644 index 48d746c7..00000000 --- a/pkg/helm/cmd/helm/registry.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "io" - - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/pkg/action" -) - -const registryHelp = ` -This command consists of multiple subcommands to interact with registries. -` - -func newRegistryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - cmd := &cobra.Command{ - Use: "registry", - Short: "login to or logout from a registry", - Long: registryHelp, - } - cmd.AddCommand( - newRegistryLoginCmd(cfg, out), - newRegistryLogoutCmd(cfg, out), - ) - return cmd -} diff --git a/pkg/helm/cmd/helm/registry_login_test.go b/pkg/helm/cmd/helm/registry_login_test.go deleted file mode 100644 index 9130240f..00000000 --- a/pkg/helm/cmd/helm/registry_login_test.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "testing" -) - -func TestRegistryLoginFileCompletion(t *testing.T) { - checkFileCompletion(t, "registry login", false) -} diff --git a/pkg/helm/cmd/helm/registry_logout_test.go b/pkg/helm/cmd/helm/registry_logout_test.go deleted file mode 100644 index 2531df26..00000000 --- a/pkg/helm/cmd/helm/registry_logout_test.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "testing" -) - -func TestRegistryLogoutFileCompletion(t *testing.T) { - checkFileCompletion(t, "registry logout", false) -} diff --git a/pkg/helm/cmd/helm/repo.go b/pkg/helm/cmd/helm/repo.go deleted file mode 100644 index a566cfb4..00000000 --- a/pkg/helm/cmd/helm/repo.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "io" - "os" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/cmd/helm/require" -) - -var repoHelm = ` -This command consists of multiple subcommands to interact with chart repositories. - -It can be used to add, remove, list, and index chart repositories. -` - -func newRepoCmd(out io.Writer) *cobra.Command { - cmd := &cobra.Command{ - Use: "repo add|remove|list|index|update [ARGS]", - Short: "add, list, remove, update, and index chart repositories", - Long: repoHelm, - Args: require.NoArgs, - } - - cmd.AddCommand(newRepoAddCmd(out)) - cmd.AddCommand(newRepoRemoveCmd(out)) - cmd.AddCommand(newRepoUpdateCmd(out)) - - return cmd -} - -func isNotExist(err error) bool { - return os.IsNotExist(errors.Cause(err)) -} diff --git a/pkg/helm/cmd/helm/repo_test.go b/pkg/helm/cmd/helm/repo_test.go deleted file mode 100644 index bc63d7b4..00000000 --- a/pkg/helm/cmd/helm/repo_test.go +++ /dev/null @@ -1,25 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "testing" -) - -func TestRepoFileCompletion(t *testing.T) { - checkFileCompletion(t, "repo", false) -} diff --git a/pkg/helm/cmd/helm/repo_update.go b/pkg/helm/cmd/helm/repo_update.go deleted file mode 100644 index 51a93841..00000000 --- a/pkg/helm/cmd/helm/repo_update.go +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "io" - "sync" - - "github.com/pkg/errors" - "github.com/spf13/cobra" - - "github.com/werf/nelm/pkg/helm/cmd/helm/require" - "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -const updateDesc = ` -Update gets the latest information about charts from the respective chart repositories. -Information is cached locally, where it is used by commands like 'helm search'. - -You can optionally specify a list of repositories you want to update. - $ helm repo update ... -To update all the repositories, use 'helm repo update'. -` - -var errNoRepositories = errors.New("no repositories found. You must add one before updating") - -type repoUpdateOptions struct { - update func([]*repo.ChartRepository, io.Writer, bool) error - repoFile string - repoCache string - names []string - failOnRepoUpdateFail bool -} - -func newRepoUpdateCmd(out io.Writer) *cobra.Command { - o := &repoUpdateOptions{update: updateCharts} - - cmd := &cobra.Command{ - Use: "update [REPO1 [REPO2 ...]]", - Aliases: []string{"up"}, - Short: "update information of available charts locally from chart repositories", - Long: updateDesc, - Args: require.MinimumNArgs(0), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp - }, - RunE: func(cmd *cobra.Command, args []string) error { - o.repoFile = settings.RepositoryConfig - o.repoCache = settings.RepositoryCache - o.names = args - return o.run(out) - }, - } - - f := cmd.Flags() - - // Adding this flag for Helm 3 as stop gap functionality for https://github.com/helm/helm/issues/10016. - // This should be deprecated in Helm 4 by update to the behaviour of `helm repo update` command. - f.BoolVar(&o.failOnRepoUpdateFail, "fail-on-repo-update-fail", false, "update fails if any of the repository updates fail") - - return cmd -} - -func (o *repoUpdateOptions) run(out io.Writer) error { - f, err := repo.LoadFile(o.repoFile) - switch { - case isNotExist(err): - return errNoRepositories - case err != nil: - return errors.Wrapf(err, "failed loading file: %s", o.repoFile) - case len(f.Repositories) == 0: - return errNoRepositories - } - - var repos []*repo.ChartRepository - updateAllRepos := len(o.names) == 0 - - if !updateAllRepos { - // Fail early if the user specified an invalid repo to update - if err := checkRequestedRepos(o.names, f.Repositories); err != nil { - return err - } - } - - for _, cfg := range f.Repositories { - if updateAllRepos || isRepoRequested(cfg.Name, o.names) { - r, err := repo.NewChartRepository(cfg, getter.All(settings)) - if err != nil { - return err - } - if o.repoCache != "" { - r.CachePath = o.repoCache - } - repos = append(repos, r) - } - } - - return o.update(repos, out, o.failOnRepoUpdateFail) -} - -func updateCharts(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { - fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") - var wg sync.WaitGroup - var repoFailList []string - for _, re := range repos { - wg.Add(1) - go func(re *repo.ChartRepository) { - defer wg.Done() - if _, err := re.DownloadIndexFile(); err != nil { - fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) - repoFailList = append(repoFailList, re.Config.URL) - } else { - fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) - } - }(re) - } - wg.Wait() - - if len(repoFailList) > 0 && failOnRepoUpdateFail { - return fmt.Errorf("Failed to update the following repositories: %s", - repoFailList) - } - - fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") - return nil -} - -func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error { - for _, requestedRepo := range requestedRepos { - found := false - for _, repo := range validRepos { - if requestedRepo == repo.Name { - found = true - break - } - } - if !found { - return errors.Errorf("no repositories found matching '%s'. Nothing will be updated", requestedRepo) - } - } - return nil -} - -func isRepoRequested(repoName string, requestedRepos []string) bool { - for _, requestedRepo := range requestedRepos { - if repoName == requestedRepo { - return true - } - } - return false -} diff --git a/pkg/helm/cmd/helm/repo_update_test.go b/pkg/helm/cmd/helm/repo_update_test.go deleted file mode 100644 index 2e87136a..00000000 --- a/pkg/helm/cmd/helm/repo_update_test.go +++ /dev/null @@ -1,238 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "bytes" - "fmt" - "io" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/werf/nelm/pkg/helm/intern/test/ensure" - "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" -) - -func TestUpdateCmd(t *testing.T) { - var out bytes.Buffer - // Instead of using the HTTP updater, we provide our own for this test. - // The TestUpdateCharts test verifies the HTTP behavior independently. - updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { - for _, re := range repos { - fmt.Fprintln(out, re.Config.Name) - } - return nil - } - o := &repoUpdateOptions{ - update: updater, - repoFile: "testdata/repositories.yaml", - } - if err := o.run(&out); err != nil { - t.Fatal(err) - } - - if got := out.String(); !strings.Contains(got, "charts") || - !strings.Contains(got, "firstexample") || - !strings.Contains(got, "secondexample") { - t.Errorf("Expected 'charts', 'firstexample' and 'secondexample' but got %q", got) - } -} - -func TestUpdateCmdMultiple(t *testing.T) { - var out bytes.Buffer - // Instead of using the HTTP updater, we provide our own for this test. - // The TestUpdateCharts test verifies the HTTP behavior independently. - updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { - for _, re := range repos { - fmt.Fprintln(out, re.Config.Name) - } - return nil - } - o := &repoUpdateOptions{ - update: updater, - repoFile: "testdata/repositories.yaml", - names: []string{"firstexample", "charts"}, - } - if err := o.run(&out); err != nil { - t.Fatal(err) - } - - if got := out.String(); !strings.Contains(got, "charts") || - !strings.Contains(got, "firstexample") || - strings.Contains(got, "secondexample") { - t.Errorf("Expected 'charts' and 'firstexample' but not 'secondexample' but got %q", got) - } -} - -func TestUpdateCmdInvalid(t *testing.T) { - var out bytes.Buffer - // Instead of using the HTTP updater, we provide our own for this test. - // The TestUpdateCharts test verifies the HTTP behavior independently. - updater := func(repos []*repo.ChartRepository, out io.Writer, failOnRepoUpdateFail bool) error { - for _, re := range repos { - fmt.Fprintln(out, re.Config.Name) - } - return nil - } - o := &repoUpdateOptions{ - update: updater, - repoFile: "testdata/repositories.yaml", - names: []string{"firstexample", "invalid"}, - } - if err := o.run(&out); err == nil { - t.Fatal("expected error but did not get one") - } -} - -func TestUpdateCustomCacheCmd(t *testing.T) { - rootDir := t.TempDir() - cachePath := filepath.Join(rootDir, "updcustomcache") - os.Mkdir(cachePath, os.ModePerm) - - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - defer ts.Stop() - - o := &repoUpdateOptions{ - update: updateCharts, - repoFile: filepath.Join(ts.Root(), "repositories.yaml"), - repoCache: cachePath, - } - b := io.Discard - if err := o.run(b); err != nil { - t.Fatal(err) - } - if _, err := os.Stat(filepath.Join(cachePath, "test-index.yaml")); err != nil { - t.Fatalf("error finding created index file in custom cache: %v", err) - } -} - -func TestUpdateCharts(t *testing.T) { - defer resetEnv()() - ensure.HelmHome(t) - - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - defer ts.Stop() - - r, err := repo.NewChartRepository(&repo.Entry{ - Name: "charts", - URL: ts.URL(), - }, getter.All(settings)) - if err != nil { - t.Error(err) - } - - b := bytes.NewBuffer(nil) - updateCharts([]*repo.ChartRepository{r}, b, false) - - got := b.String() - if strings.Contains(got, "Unable to get an update") { - t.Errorf("Failed to get a repo: %q", got) - } - if !strings.Contains(got, "Update Complete.") { - t.Error("Update was not successful") - } -} - -func TestRepoUpdateFileCompletion(t *testing.T) { - checkFileCompletion(t, "repo update", false) - checkFileCompletion(t, "repo update repo1", false) -} - -func TestUpdateChartsFail(t *testing.T) { - defer resetEnv()() - ensure.HelmHome(t) - - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - defer ts.Stop() - - var invalidURL = ts.URL() + "55" - r, err := repo.NewChartRepository(&repo.Entry{ - Name: "charts", - URL: invalidURL, - }, getter.All(settings)) - if err != nil { - t.Error(err) - } - - b := bytes.NewBuffer(nil) - if err := updateCharts([]*repo.ChartRepository{r}, b, false); err != nil { - t.Error("Repo update should not return error if update of repository fails") - } - - got := b.String() - if !strings.Contains(got, "Unable to get an update") { - t.Errorf("Repo should have failed update but instead got: %q", got) - } - if !strings.Contains(got, "Update Complete.") { - t.Error("Update was not successful") - } -} - -func TestUpdateChartsFailWithError(t *testing.T) { - defer resetEnv()() - ensure.HelmHome(t) - - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } - defer ts.Stop() - - var invalidURL = ts.URL() + "55" - r, err := repo.NewChartRepository(&repo.Entry{ - Name: "charts", - URL: invalidURL, - }, getter.All(settings)) - if err != nil { - t.Error(err) - } - - b := bytes.NewBuffer(nil) - err = updateCharts([]*repo.ChartRepository{r}, b, true) - if err == nil { - t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set") - return - } - var expectedErr = "Failed to update the following repositories" - var receivedErr = err.Error() - if !strings.Contains(receivedErr, expectedErr) { - t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr) - } - if !strings.Contains(receivedErr, invalidURL) { - t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr) - } - - got := b.String() - if !strings.Contains(got, "Unable to get an update") { - t.Errorf("Repo should have failed update but instead got: %q", got) - } - if strings.Contains(got, "Update Complete.") { - t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set") - } -} diff --git a/pkg/helm/cmd/helm/root.go b/pkg/helm/cmd/helm/root.go deleted file mode 100644 index 8749b4d5..00000000 --- a/pkg/helm/cmd/helm/root.go +++ /dev/null @@ -1,283 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm // import "helm.sh/helm/v3/cmd/helm" - -import ( - "context" - "fmt" - "io" - "log" - "os" - "strings" - - "github.com/spf13/cobra" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/clientcmd" - - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -var globalUsage = `The Kubernetes package manager - -Common actions for Helm: - -- helm search: search for charts -- helm pull: download a chart to your local directory to view -- helm install: upload the chart to Kubernetes -- helm list: list releases of charts - -Environment variables: - -| Name | Description | -|------------------------------------|------------------------------------------------------------------------------------------------------------| -| $HELM_CACHE_HOME | set an alternative location for storing cached files. | -| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | -| $HELM_DATA_HOME | set an alternative location for storing Helm data. | -| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | -| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. | -| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | -| $HELM_MAX_HISTORY | set the maximum number of helm release history. | -| $HELM_NAMESPACE | set the namespace used for the helm operations. | -| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | -| $HELM_PLUGINS | set the path to the plugins directory | -| $HELM_REGISTRY_CONFIG | set the path to the registry config file. | -| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory | -| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. | -| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | -| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication | -| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. | -| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. | -| $HELM_KUBEASUSER | set the Username to impersonate for the operation. | -| $HELM_KUBECONTEXT | set the name of the kubeconfig context. | -| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | -| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) | -| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate | -| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable) | -| $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values | - -Helm stores cache, configuration, and data based on the following configuration order: - -- If a HELM_*_HOME environment variable is set, it will be used -- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used -- When no other location is set a default location will be used based on the operating system - -By default, the default directories depend on the Operating System. The defaults are listed below: - -| Operating System | Cache Path | Configuration Path | Data Path | -|------------------|---------------------------|--------------------------------|-------------------------| -| Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm | -| macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm | -| Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm | -` - -func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) (*cobra.Command, error) { - cmd := &cobra.Command{ - Use: "helm", - Short: "The Helm package manager for Kubernetes.", - Long: globalUsage, - SilenceUsage: true, - } - flags := cmd.PersistentFlags() - - settings.AddFlags(flags) - addKlogFlags(flags) - - // Setup shell completion for the namespace flag - err := cmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if client, err := actionConfig.KubernetesClientSet(); err == nil { - // Choose a long enough timeout that the user notices something is not working - // but short enough that the user is not made to wait very long - to := int64(3) - cobra.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to), settings.Debug) - - nsNames := []string{} - if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { - for _, ns := range namespaces.Items { - nsNames = append(nsNames, ns.Name) - } - return nsNames, cobra.ShellCompDirectiveNoFileComp - } - } - return nil, cobra.ShellCompDirectiveDefault - }) - - if err != nil { - log.Fatal(err) - } - - // Setup shell completion for the kube-context flag - err = cmd.RegisterFlagCompletionFunc("kube-context", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - cobra.CompDebugln("About to get the different kube-contexts", settings.Debug) - - loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() - if len(settings.KubeConfig) > 0 { - loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig} - } - if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( - loadingRules, - &clientcmd.ConfigOverrides{}).RawConfig(); err == nil { - comps := []string{} - for name, context := range config.Contexts { - comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster)) - } - return comps, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }) - - if err != nil { - log.Fatal(err) - } - - // We can safely ignore any errors that flags.Parse encounters since - // those errors will be caught later during the call to cmd.Execution. - // This call is required to gather configuration information prior to - // execution. - flags.ParseErrorsWhitelist.UnknownFlags = true - // flags.Parse(args) - - registryClient, err := newDefaultRegistryClient(false) - if err != nil { - return nil, err - } - actionConfig.RegistryClient = registryClient - - // Add subcommands - cmd.AddCommand( - newDependencyCmd(actionConfig, out), - newPullCmd(actionConfig, out), - newPackageCmd(actionConfig, out), - newRepoCmd(out), - newSearchCmd(out), - newVerifyCmd(out), - - newHistoryCmd(actionConfig, out), - newListCmd(actionConfig, out), - - newCompletionCmd(out), - - newRegistryCmd(actionConfig, out), - newPushCmd(actionConfig, out), - ) - - // Find and add plugins - // loadPlugins(cmd, out) - - // Check permissions on critical files - checkPerms() - - // Check for expired repositories - checkForExpiredRepos(settings.RepositoryConfig) - - return cmd, nil -} - -func checkForExpiredRepos(repofile string) { - - expiredRepos := []struct { - name string - old string - new string - }{ - { - name: "stable", - old: "kubernetes-charts.storage.googleapis.com", - new: "https://charts.helm.sh/stable", - }, - { - name: "incubator", - old: "kubernetes-charts-incubator.storage.googleapis.com", - new: "https://charts.helm.sh/incubator", - }, - } - - // parse repo file. - // Ignore the error because it is okay for a repo file to be unparseable at this - // stage. Later checks will trap the error and respond accordingly. - repoFile, err := repo.LoadFile(repofile) - if err != nil { - return - } - - for _, exp := range expiredRepos { - r := repoFile.Get(exp.name) - if r == nil { - return - } - - if url := r.URL; strings.Contains(url, exp.old) { - fmt.Fprintf( - os.Stderr, - "WARNING: %q is deprecated for %q and will be deleted Nov. 13, 2020.\nWARNING: You should switch to %q via:\nWARNING: helm repo add %q %q --force-update\n", - exp.old, - exp.name, - exp.new, - exp.name, - exp.new, - ) - } - } - -} - -func newRegistryClient(certFile, keyFile, caFile string, insecureSkipTLSverify, plainHTTP bool) (*registry.Client, error) { - if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSverify { - registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSverify) - if err != nil { - return nil, err - } - return registryClient, nil - } - registryClient, err := newDefaultRegistryClient(plainHTTP) - if err != nil { - return nil, err - } - return registryClient, nil -} - -func newDefaultRegistryClient(plainHTTP bool) (*registry.Client, error) { - opts := []registry.ClientOption{ - registry.ClientOptDebug(settings.Debug), - registry.ClientOptEnableCache(true), - registry.ClientOptWriter(os.Stderr), - registry.ClientOptCredentialsFile(settings.RegistryConfig), - } - if plainHTTP { - opts = append(opts, registry.ClientOptPlainHTTP()) - } - - // Create a new registry client - registryClient, err := registry.NewClient(opts...) - if err != nil { - return nil, err - } - return registryClient, nil -} - -func newRegistryClientWithTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*registry.Client, error) { - // Create a new registry client - registryClient, err := registry.NewRegistryClientWithTLS(os.Stderr, certFile, keyFile, caFile, insecureSkipTLSverify, - settings.RegistryConfig, settings.Debug, - ) - if err != nil { - return nil, err - } - return registryClient, nil -} diff --git a/pkg/helm/cmd/helm/root_unix.go b/pkg/helm/cmd/helm/root_unix.go deleted file mode 100644 index 9879ec26..00000000 --- a/pkg/helm/cmd/helm/root_unix.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build !windows - -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "os" - "os/user" - "path/filepath" -) - -func checkPerms() { - // This function MUST NOT FAIL, as it is just a check for a common permissions problem. - // If for some reason the function hits a stopping condition, it may panic. But only if - // we can be sure that it is panicking because Helm cannot proceed. - - kc := settings.KubeConfig - if kc == "" { - kc = os.Getenv("KUBECONFIG") - } - if kc == "" { - u, err := user.Current() - if err != nil { - // No idea where to find KubeConfig, so return silently. Many helm commands - // can proceed happily without a KUBECONFIG, so this is not a fatal error. - return - } - kc = filepath.Join(u.HomeDir, ".kube", "config") - } - fi, err := os.Stat(kc) - if err != nil { - // DO NOT error if no KubeConfig is found. Not all commands require one. - return - } - - perm := fi.Mode().Perm() - if perm&0040 > 0 { - warning("Kubernetes configuration file is group-readable. This is insecure. Location: %s", kc) - } - if perm&0004 > 0 { - warning("Kubernetes configuration file is world-readable. This is insecure. Location: %s", kc) - } -} diff --git a/pkg/helm/cmd/helm/root_unix_test.go b/pkg/helm/cmd/helm/root_unix_test.go deleted file mode 100644 index 4301aa6a..00000000 --- a/pkg/helm/cmd/helm/root_unix_test.go +++ /dev/null @@ -1,82 +0,0 @@ -//go:build !windows - -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "bytes" - "io" - "os" - "path/filepath" - "strings" - "testing" -) - -func checkPermsStderr() (string, error) { - r, w, err := os.Pipe() - if err != nil { - return "", err - } - - stderr := os.Stderr - os.Stderr = w - defer func() { - os.Stderr = stderr - }() - - checkPerms() - w.Close() - - var text bytes.Buffer - io.Copy(&text, r) - return text.String(), nil -} - -func TestCheckPerms(t *testing.T) { - tdir := t.TempDir() - tfile := filepath.Join(tdir, "testconfig") - fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440) - if err != nil { - t.Errorf("Failed to create temp file: %s", err) - } - - tconfig := settings.KubeConfig - settings.KubeConfig = tfile - defer func() { settings.KubeConfig = tconfig }() - - text, err := checkPermsStderr() - if err != nil { - t.Fatalf("could not read from stderr: %s", err) - } - expectPrefix := "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location:" - if !strings.HasPrefix(text, expectPrefix) { - t.Errorf("Expected to get a warning for group perms. Got %q", text) - } - - if err := fh.Chmod(0404); err != nil { - t.Errorf("Could not change mode on file: %s", err) - } - text, err = checkPermsStderr() - if err != nil { - t.Fatalf("could not read from stderr: %s", err) - } - expectPrefix = "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location:" - if !strings.HasPrefix(text, expectPrefix) { - t.Errorf("Expected to get a warning for world perms. Got %q", text) - } -} diff --git a/pkg/helm/cmd/helm/root_windows.go b/pkg/helm/cmd/helm/root_windows.go deleted file mode 100644 index 7c3ef7f4..00000000 --- a/pkg/helm/cmd/helm/root_windows.go +++ /dev/null @@ -1,22 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -func checkPerms() { - // Not yet implemented on Windows. If you know how to do a comprehensive perms - // check on Windows, contributions welcomed! -} diff --git a/pkg/helm/cmd/helm/search.go b/pkg/helm/cmd/helm/search.go deleted file mode 100644 index 9a71f4cd..00000000 --- a/pkg/helm/cmd/helm/search.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "io" - - "github.com/spf13/cobra" -) - -const searchDesc = ` -Search provides the ability to search for Helm charts in the various places -they can be stored including the Artifact Hub and repositories you have added. -Use search subcommands to search different locations for charts. -` - -func newSearchCmd(out io.Writer) *cobra.Command { - - cmd := &cobra.Command{ - Use: "search [keyword]", - Short: "search for a keyword in charts", - Long: searchDesc, - } - - cmd.AddCommand(newSearchHubCmd(out)) - cmd.AddCommand(newSearchRepoCmd(out)) - - return cmd -} diff --git a/pkg/helm/cmd/helm/search/search.go b/pkg/helm/cmd/helm/search/search.go deleted file mode 100644 index 3fc8bc9c..00000000 --- a/pkg/helm/cmd/helm/search/search.go +++ /dev/null @@ -1,227 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* -Package search provides client-side repository searching. - -This supports building an in-memory search index based on the contents of -multiple repositories, and then using string matching or regular expressions -to find matches. -*/ -package search - -import ( - "path" - "regexp" - "sort" - "strings" - - "github.com/Masterminds/semver/v3" - - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -// Result is a search result. -// -// Score indicates how close it is to match. The higher the score, the longer -// the distance. -type Result struct { - Name string - Score int - Chart *repo.ChartVersion -} - -// Index is a searchable index of chart information. -type Index struct { - lines map[string]string - charts map[string]*repo.ChartVersion -} - -const sep = "\v" - -// NewIndex creates a new Index. -func NewIndex() *Index { - return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} -} - -// verSep is a separator for version fields in map keys. -const verSep = "$$" - -// AddRepo adds a repository index to the search index. -func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { - ind.SortEntries() - for name, ref := range ind.Entries { - if len(ref) == 0 { - // Skip chart names that have zero releases. - continue - } - // By convention, an index file is supposed to have the newest at the - // 0 slot, so our best bet is to grab the 0 entry and build the index - // entry off of that. - // Note: Do not use filePath.Join since on Windows it will return \ - // which results in a repo name that cannot be understood. - fname := path.Join(rname, name) - if !all { - i.lines[fname] = indstr(rname, ref[0]) - i.charts[fname] = ref[0] - continue - } - - // If 'all' is set, then we go through all of the refs, and add them all - // to the index. This will generate a lot of near-duplicate entries. - for _, rr := range ref { - versionedName := fname + verSep + rr.Version - i.lines[versionedName] = indstr(rname, rr) - i.charts[versionedName] = rr - } - } -} - -// All returns all charts in the index as if they were search results. -// -// Each will be given a score of 0. -func (i *Index) All() []*Result { - res := make([]*Result, len(i.charts)) - j := 0 - for name, ch := range i.charts { - parts := strings.Split(name, verSep) - res[j] = &Result{ - Name: parts[0], - Chart: ch, - } - j++ - } - return res -} - -// Search searches an index for the given term. -// -// Threshold indicates the maximum score a term may have before being marked -// irrelevant. (Low score means higher relevance. Golf, not bowling.) -// -// If regexp is true, the term is treated as a regular expression. Otherwise, -// term is treated as a literal string. -func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { - if regexp { - return i.SearchRegexp(term, threshold) - } - return i.SearchLiteral(term, threshold), nil -} - -// calcScore calculates a score for a match. -func (i *Index) calcScore(index int, matchline string) int { - - // This is currently tied to the fact that sep is a single char. - splits := []int{} - s := rune(sep[0]) - for i, ch := range matchline { - if ch == s { - splits = append(splits, i) - } - } - - for i, pos := range splits { - if index > pos { - continue - } - return i - } - return len(splits) -} - -// SearchLiteral does a literal string search (no regexp). -func (i *Index) SearchLiteral(term string, threshold int) []*Result { - term = strings.ToLower(term) - buf := []*Result{} - for k, v := range i.lines { - lv := strings.ToLower(v) - res := strings.Index(lv, term) - if score := i.calcScore(res, lv); res != -1 && score < threshold { - parts := strings.Split(k, verSep) // Remove version, if it is there. - buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) - } - } - return buf -} - -// SearchRegexp searches using a regular expression. -func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { - matcher, err := regexp.Compile(re) - if err != nil { - return []*Result{}, err - } - buf := []*Result{} - for k, v := range i.lines { - ind := matcher.FindStringIndex(v) - if len(ind) == 0 { - continue - } - if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { - parts := strings.Split(k, verSep) // Remove version, if it is there. - buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) - } - } - return buf, nil -} - -// SortScore does an in-place sort of the results. -// -// Lowest scores are highest on the list. Matching scores are subsorted alphabetically. -func SortScore(r []*Result) { - sort.Sort(scoreSorter(r)) -} - -// scoreSorter sorts results by score, and subsorts by alpha Name. -type scoreSorter []*Result - -// Len returns the length of this scoreSorter. -func (s scoreSorter) Len() int { return len(s) } - -// Swap performs an in-place swap. -func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Less compares a to b, and returns true if a is less than b. -func (s scoreSorter) Less(a, b int) bool { - first := s[a] - second := s[b] - - if first.Score > second.Score { - return false - } - if first.Score < second.Score { - return true - } - if first.Name == second.Name { - v1, err := semver.NewVersion(first.Chart.Version) - if err != nil { - return true - } - v2, err := semver.NewVersion(second.Chart.Version) - if err != nil { - return true - } - // Sort so that the newest chart is higher than the oldest chart. This is - // the opposite of what you'd expect in a function called Less. - return v1.GreaterThan(v2) - } - return first.Name < second.Name -} - -func indstr(name string, ref *repo.ChartVersion) string { - i := ref.Name + sep + name + "/" + ref.Name + sep + - ref.Description + sep + strings.Join(ref.Keywords, " ") - return i -} diff --git a/pkg/helm/cmd/helm/search/search_test.go b/pkg/helm/cmd/helm/search/search_test.go deleted file mode 100644 index 3d6ca30c..00000000 --- a/pkg/helm/cmd/helm/search/search_test.go +++ /dev/null @@ -1,311 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package search - -import ( - "strings" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -func TestSortScore(t *testing.T) { - in := []*Result{ - {Name: "bbb", Score: 0, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, - {Name: "aaa", Score: 5}, - {Name: "abb", Score: 5}, - {Name: "aab", Score: 0}, - {Name: "bab", Score: 5}, - {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.4"}}}, - {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, - } - expect := []string{"aab", "bbb", "aaa", "abb", "bab", "ver", "ver"} - expectScore := []int{0, 0, 5, 5, 5, 5, 5} - SortScore(in) - - // Test Score - for i := 0; i < len(expectScore); i++ { - if expectScore[i] != in[i].Score { - t.Errorf("Sort error on index %d: expected %d, got %d", i, expectScore[i], in[i].Score) - } - } - // Test Name - for i := 0; i < len(expect); i++ { - if expect[i] != in[i].Name { - t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name) - } - } - - // Test version of last two items - if in[5].Chart.Version != "1.2.4" { - t.Errorf("Expected 1.2.4, got %s", in[5].Chart.Version) - } - if in[6].Chart.Version != "1.2.3" { - t.Error("Expected 1.2.3 to be last") - } -} - -var indexfileEntries = map[string]repo.ChartVersions{ - "niña": { - { - URLs: []string{"http://example.com/charts/nina-0.1.0.tgz"}, - Metadata: &chart.Metadata{ - Name: "niña", - Version: "0.1.0", - Description: "One boat", - }, - }, - }, - "pinta": { - { - URLs: []string{"http://example.com/charts/pinta-0.1.0.tgz"}, - Metadata: &chart.Metadata{ - Name: "pinta", - Version: "0.1.0", - Description: "Two ship", - }, - }, - }, - "santa-maria": { - { - URLs: []string{"http://example.com/charts/santa-maria-1.2.3.tgz"}, - Metadata: &chart.Metadata{ - Name: "santa-maria", - Version: "1.2.3", - Description: "Three boat", - }, - }, - { - URLs: []string{"http://example.com/charts/santa-maria-1.2.2-rc-1.tgz"}, - Metadata: &chart.Metadata{ - Name: "santa-maria", - Version: "1.2.2-RC-1", - Description: "Three boat", - }, - }, - }, -} - -func loadTestIndex(_ *testing.T, all bool) *Index { - i := NewIndex() - i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) - i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ - "Pinta": { - { - URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, - Metadata: &chart.Metadata{ - Name: "Pinta", - Version: "2.0.0", - Description: "Two ship, version two", - }, - }, - }, - }}, all) - return i -} - -func TestAll(t *testing.T) { - i := loadTestIndex(t, false) - all := i.All() - if len(all) != 4 { - t.Errorf("Expected 4 entries, got %d", len(all)) - } - - i = loadTestIndex(t, true) - all = i.All() - if len(all) != 5 { - t.Errorf("Expected 5 entries, got %d", len(all)) - } -} - -func TestAddRepo_Sort(t *testing.T) { - i := loadTestIndex(t, true) - sr, err := i.Search("TESTING/SANTA-MARIA", 100, false) - if err != nil { - t.Fatal(err) - } - SortScore(sr) - - ch := sr[0] - expect := "1.2.3" - if ch.Chart.Version != expect { - t.Errorf("Expected %q, got %q", expect, ch.Chart.Version) - } -} - -func TestSearchByName(t *testing.T) { - - tests := []struct { - name string - query string - expect []*Result - regexp bool - fail bool - failMsg string - }{ - { - name: "basic search for one result", - query: "santa-maria", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - }, - { - name: "basic search for two results", - query: "pinta", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/Pinta"}, - }, - }, - { - name: "repo-specific search for one result", - query: "ztesting/pinta", - expect: []*Result{ - {Name: "ztesting/Pinta"}, - }, - }, - { - name: "partial name search", - query: "santa", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - }, - { - name: "description search, one result", - query: "Three", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - }, - { - name: "description search, two results", - query: "two", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/Pinta"}, - }, - }, - { - name: "search mixedCase and result should be mixedCase too", - query: "pinta", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/Pinta"}, - }, - }, - { - name: "description upper search, two results", - query: "TWO", - expect: []*Result{ - {Name: "testing/pinta"}, - {Name: "ztesting/Pinta"}, - }, - }, - { - name: "nothing found", - query: "mayflower", - expect: []*Result{}, - }, - { - name: "regexp, one result", - query: "Th[ref]*", - expect: []*Result{ - {Name: "testing/santa-maria"}, - }, - regexp: true, - }, - { - name: "regexp, fail compile", - query: "th[", - expect: []*Result{}, - regexp: true, - fail: true, - failMsg: "error parsing regexp:", - }, - } - - i := loadTestIndex(t, false) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - charts, err := i.Search(tt.query, 100, tt.regexp) - if err != nil { - if tt.fail { - if !strings.Contains(err.Error(), tt.failMsg) { - t.Fatalf("Unexpected error message: %s", err) - } - return - } - t.Fatalf("%s: %s", tt.name, err) - } - // Give us predictably ordered results. - SortScore(charts) - - l := len(charts) - if l != len(tt.expect) { - t.Fatalf("Expected %d result, got %d", len(tt.expect), l) - } - // For empty result sets, just keep going. - if l == 0 { - return - } - - for i, got := range charts { - ex := tt.expect[i] - if got.Name != ex.Name { - t.Errorf("[%d]: Expected name %q, got %q", i, ex.Name, got.Name) - } - } - - }) - } -} - -func TestSearchByNameAll(t *testing.T) { - // Test with the All bit turned on. - i := loadTestIndex(t, true) - cs, err := i.Search("santa-maria", 100, false) - if err != nil { - t.Fatal(err) - } - if len(cs) != 2 { - t.Errorf("expected 2 charts, got %d", len(cs)) - } -} - -func TestCalcScore(t *testing.T) { - i := NewIndex() - - fields := []string{"aaa", "bbb", "ccc", "ddd"} - matchline := strings.Join(fields, sep) - if r := i.calcScore(2, matchline); r != 0 { - t.Errorf("Expected 0, got %d", r) - } - if r := i.calcScore(5, matchline); r != 1 { - t.Errorf("Expected 1, got %d", r) - } - if r := i.calcScore(10, matchline); r != 2 { - t.Errorf("Expected 2, got %d", r) - } - if r := i.calcScore(14, matchline); r != 3 { - t.Errorf("Expected 3, got %d", r) - } -} diff --git a/pkg/helm/cmd/helm/search_test.go b/pkg/helm/cmd/helm/search_test.go deleted file mode 100644 index 636bf5b2..00000000 --- a/pkg/helm/cmd/helm/search_test.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import "testing" - -func TestSearchFileCompletion(t *testing.T) { - checkFileCompletion(t, "search", false) -} diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml b/pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml deleted file mode 100644 index 63f2f12d..00000000 --- a/pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: fullenv -usage: "show env vars" -description: "show all env vars" -command: "$HELM_PLUGIN_DIR/fullenv.sh" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/args.sh b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/args.sh deleted file mode 100755 index 678b4eff..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/args.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -echo $* diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.yaml b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.yaml deleted file mode 100644 index 21e28a7c..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: args -usage: "echo args" -description: "This echos args" -command: "$HELM_PLUGIN_DIR/args.sh" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.yaml b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.yaml deleted file mode 100644 index 7b9362a0..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: echo -usage: "echo stuff" -description: "This echos stuff" -command: "echo hello" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml deleted file mode 100644 index e479a050..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml +++ /dev/null @@ -1,13 +0,0 @@ -name: env -commands: - - name: list - flags: - - a - - all - - log - - name: remove - validArgs: - - all - - one -flags: -- global diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.yaml b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.yaml deleted file mode 100644 index 52cb7a84..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/env/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: env -usage: "env stuff" -description: "show the env" -command: "echo $HELM_PLUGIN_NAME" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/exitwith.sh b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/exitwith.sh deleted file mode 100755 index ec846965..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/exitwith.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exit $* diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/plugin.yaml b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/plugin.yaml deleted file mode 100644 index 5691d171..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: exitwith -usage: "exitwith code" -description: "This exits with the specified exit code" -command: "$HELM_PLUGIN_DIR/exitwith.sh" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/fullenv.sh b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/fullenv.sh deleted file mode 100755 index 2efad9b3..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/fullenv.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh -echo $HELM_PLUGIN_NAME -echo $HELM_PLUGIN_DIR -echo $HELM_PLUGINS -echo $HELM_REPOSITORY_CONFIG -echo $HELM_REPOSITORY_CACHE -echo $HELM_BIN diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/plugin.yaml b/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/plugin.yaml deleted file mode 100644 index 63f2f12d..00000000 --- a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: fullenv -usage: "show env vars" -description: "show all env vars" -command: "$HELM_PLUGIN_DIR/fullenv.sh" diff --git a/pkg/helm/cmd/helm/testdata/output/empty_nofile_comp.txt b/pkg/helm/cmd/helm/testdata/output/empty_nofile_comp.txt deleted file mode 100644 index 8d9fad57..00000000 --- a/pkg/helm/cmd/helm/testdata/output/empty_nofile_comp.txt +++ /dev/null @@ -1,2 +0,0 @@ -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/get-all-no-args.txt b/pkg/helm/cmd/helm/testdata/output/get-all-no-args.txt deleted file mode 100644 index cc3fc2ad..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-all-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm get all" requires 1 argument - -Usage: helm get all RELEASE_NAME [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/get-hooks-no-args.txt b/pkg/helm/cmd/helm/testdata/output/get-hooks-no-args.txt deleted file mode 100644 index 2911fdb8..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-hooks-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm get hooks" requires 1 argument - -Usage: helm get hooks RELEASE_NAME [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/get-hooks.txt b/pkg/helm/cmd/helm/testdata/output/get-hooks.txt deleted file mode 100644 index 81e87b1f..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-hooks.txt +++ /dev/null @@ -1,8 +0,0 @@ ---- -# Source: pre-install-hook.yaml -apiVersion: v1 -kind: Job -metadata: - annotations: - "helm.sh/hook": pre-install - diff --git a/pkg/helm/cmd/helm/testdata/output/get-manifest-no-args.txt b/pkg/helm/cmd/helm/testdata/output/get-manifest-no-args.txt deleted file mode 100644 index df7aa5b0..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-manifest-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm get manifest" requires 1 argument - -Usage: helm get manifest RELEASE_NAME [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/get-manifest.txt b/pkg/helm/cmd/helm/testdata/output/get-manifest.txt deleted file mode 100644 index 88937e08..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-manifest.txt +++ /dev/null @@ -1,5 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: fixture - diff --git a/pkg/helm/cmd/helm/testdata/output/get-metadata-args.txt b/pkg/helm/cmd/helm/testdata/output/get-metadata-args.txt deleted file mode 100644 index acd3f4c1..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-metadata-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm get metadata" requires 1 argument - -Usage: helm get metadata RELEASE_NAME [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/get-metadata.json b/pkg/helm/cmd/helm/testdata/output/get-metadata.json deleted file mode 100644 index 1d5152b2..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"thomas-guide","chart":"foo","version":"0.1.0-beta.1","appVersion":"1.0","namespace":"default","revision":1,"status":"deployed","deployedAt":"1977-09-02T22:04:05Z"} diff --git a/pkg/helm/cmd/helm/testdata/output/get-metadata.txt b/pkg/helm/cmd/helm/testdata/output/get-metadata.txt deleted file mode 100644 index b91f1b86..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-metadata.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: thomas-guide -CHART: foo -VERSION: 0.1.0-beta.1 -APP_VERSION: 1.0 -NAMESPACE: default -REVISION: 1 -STATUS: deployed -DEPLOYED_AT: 1977-09-02T22:04:05Z diff --git a/pkg/helm/cmd/helm/testdata/output/get-metadata.yaml b/pkg/helm/cmd/helm/testdata/output/get-metadata.yaml deleted file mode 100644 index b6d49b03..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-metadata.yaml +++ /dev/null @@ -1,8 +0,0 @@ -appVersion: "1.0" -chart: foo -deployedAt: "1977-09-02T22:04:05Z" -name: thomas-guide -namespace: default -revision: 1 -status: deployed -version: 0.1.0-beta.1 diff --git a/pkg/helm/cmd/helm/testdata/output/get-notes-no-args.txt b/pkg/helm/cmd/helm/testdata/output/get-notes-no-args.txt deleted file mode 100644 index 1a0c20ca..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-notes-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm get notes" requires 1 argument - -Usage: helm get notes RELEASE_NAME [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/get-notes.txt b/pkg/helm/cmd/helm/testdata/output/get-notes.txt deleted file mode 100644 index e710c780..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-notes.txt +++ /dev/null @@ -1,2 +0,0 @@ -NOTES: -Some mock release notes! diff --git a/pkg/helm/cmd/helm/testdata/output/get-release-template.txt b/pkg/helm/cmd/helm/testdata/output/get-release-template.txt deleted file mode 100644 index 02d44fb0..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-release-template.txt +++ /dev/null @@ -1 +0,0 @@ -0.1.0-beta.1 \ No newline at end of file diff --git a/pkg/helm/cmd/helm/testdata/output/get-release.txt b/pkg/helm/cmd/helm/testdata/output/get-release.txt deleted file mode 100644 index 12b4a407..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-release.txt +++ /dev/null @@ -1,32 +0,0 @@ -NAME: thomas-guide -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -CHART: foo -VERSION: 0.1.0-beta.1 -APP_VERSION: 1.0 -TEST SUITE: None -USER-SUPPLIED VALUES: -name: value - -COMPUTED VALUES: -name: value - -HOOKS: ---- -# Source: pre-install-hook.yaml -apiVersion: v1 -kind: Job -metadata: - annotations: - "helm.sh/hook": pre-install - -MANIFEST: -apiVersion: v1 -kind: Secret -metadata: - name: fixture - -NOTES: -Some mock release notes! diff --git a/pkg/helm/cmd/helm/testdata/output/get-values-all.txt b/pkg/helm/cmd/helm/testdata/output/get-values-all.txt deleted file mode 100644 index b7e9696b..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-values-all.txt +++ /dev/null @@ -1,2 +0,0 @@ -COMPUTED VALUES: -name: value diff --git a/pkg/helm/cmd/helm/testdata/output/get-values-args.txt b/pkg/helm/cmd/helm/testdata/output/get-values-args.txt deleted file mode 100644 index c8a65e7f..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-values-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm get values" requires 1 argument - -Usage: helm get values RELEASE_NAME [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/get-values.txt b/pkg/helm/cmd/helm/testdata/output/get-values.txt deleted file mode 100644 index b7d146b1..00000000 --- a/pkg/helm/cmd/helm/testdata/output/get-values.txt +++ /dev/null @@ -1,2 +0,0 @@ -USER-SUPPLIED VALUES: -name: value diff --git a/pkg/helm/cmd/helm/testdata/output/install-and-replace.txt b/pkg/helm/cmd/helm/testdata/output/install-and-replace.txt deleted file mode 100644 index 7452b096..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-and-replace.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: aeneas -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-chart-bad-type.txt b/pkg/helm/cmd/helm/testdata/output/install-chart-bad-type.txt deleted file mode 100644 index c482a793..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-chart-bad-type.txt +++ /dev/null @@ -1 +0,0 @@ -Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library diff --git a/pkg/helm/cmd/helm/testdata/output/install-lib-chart.txt b/pkg/helm/cmd/helm/testdata/output/install-lib-chart.txt deleted file mode 100644 index c482a793..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-lib-chart.txt +++ /dev/null @@ -1 +0,0 @@ -Error: INSTALLATION FAILED: validation: chart.metadata.type must be application or library diff --git a/pkg/helm/cmd/helm/testdata/output/install-name-template.txt b/pkg/helm/cmd/helm/testdata/output/install-name-template.txt deleted file mode 100644 index d5e20ba8..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-name-template.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: foobar -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-no-args.txt b/pkg/helm/cmd/helm/testdata/output/install-no-args.txt deleted file mode 100644 index 47f010ab..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm install" requires at least 1 argument - -Usage: helm install [NAME] [CHART] [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/install-no-hooks.txt b/pkg/helm/cmd/helm/testdata/output/install-no-hooks.txt deleted file mode 100644 index 7452b096..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-no-hooks.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: aeneas -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-multiple-values-files.txt b/pkg/helm/cmd/helm/testdata/output/install-with-multiple-values-files.txt deleted file mode 100644 index c8e7b04d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-multiple-values-files.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: virgil -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-multiple-values.txt b/pkg/helm/cmd/helm/testdata/output/install-with-multiple-values.txt deleted file mode 100644 index c8e7b04d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-multiple-values.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: virgil -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-timeout.txt b/pkg/helm/cmd/helm/testdata/output/install-with-timeout.txt deleted file mode 100644 index d5e20ba8..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-timeout.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: foobar -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-values-file.txt b/pkg/helm/cmd/helm/testdata/output/install-with-values-file.txt deleted file mode 100644 index c8e7b04d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-values-file.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: virgil -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-values.txt b/pkg/helm/cmd/helm/testdata/output/install-with-values.txt deleted file mode 100644 index c8e7b04d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-values.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: virgil -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-wait-for-jobs.txt b/pkg/helm/cmd/helm/testdata/output/install-with-wait-for-jobs.txt deleted file mode 100644 index 6e93294d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-wait-for-jobs.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: apollo -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install-with-wait.txt b/pkg/helm/cmd/helm/testdata/output/install-with-wait.txt deleted file mode 100644 index 6e93294d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install-with-wait.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: apollo -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/install.txt b/pkg/helm/cmd/helm/testdata/output/install.txt deleted file mode 100644 index 7452b096..00000000 --- a/pkg/helm/cmd/helm/testdata/output/install.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: aeneas -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt b/pkg/helm/cmd/helm/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt deleted file mode 100644 index d43c7c36..00000000 --- a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt +++ /dev/null @@ -1,19 +0,0 @@ -==> Linting testdata/testcharts/chart-with-bad-subcharts -[INFO] Chart.yaml: icon is recommended -[ERROR] templates/: error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required -[ERROR] : unable to load chart - error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required - -==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart -[ERROR] Chart.yaml: name is required -[ERROR] Chart.yaml: apiVersion is required. The value must be either "v1" or "v2" -[ERROR] Chart.yaml: version is required -[INFO] Chart.yaml: icon is recommended -[ERROR] templates/: validation: chart.metadata.name is required -[ERROR] : unable to load chart - validation: chart.metadata.name is required - -==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart -[INFO] Chart.yaml: icon is recommended - -Error: 3 chart(s) linted, 2 chart(s) failed diff --git a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt b/pkg/helm/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt deleted file mode 100644 index 7c898b89..00000000 --- a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-bad-subcharts.txt +++ /dev/null @@ -1,7 +0,0 @@ -==> Linting testdata/testcharts/chart-with-bad-subcharts -[INFO] Chart.yaml: icon is recommended -[ERROR] templates/: error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required -[ERROR] : unable to load chart - error unpacking bad-subchart in chart-with-bad-subcharts: validation: chart.metadata.name is required - -Error: 1 chart(s) linted, 1 chart(s) failed diff --git a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api-old-k8s.txt b/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api-old-k8s.txt deleted file mode 100644 index bd0d7000..00000000 --- a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api-old-k8s.txt +++ /dev/null @@ -1,4 +0,0 @@ -==> Linting testdata/testcharts/chart-with-deprecated-api -[INFO] Chart.yaml: icon is recommended - -1 chart(s) linted, 0 chart(s) failed diff --git a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api-strict.txt b/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api-strict.txt deleted file mode 100644 index a1ec4394..00000000 --- a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api-strict.txt +++ /dev/null @@ -1,5 +0,0 @@ -==> Linting testdata/testcharts/chart-with-deprecated-api -[INFO] Chart.yaml: icon is recommended -[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler - -Error: 1 chart(s) linted, 1 chart(s) failed diff --git a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api.txt b/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api.txt deleted file mode 100644 index dac54620..00000000 --- a/pkg/helm/cmd/helm/testdata/output/lint-chart-with-deprecated-api.txt +++ /dev/null @@ -1,5 +0,0 @@ -==> Linting testdata/testcharts/chart-with-deprecated-api -[INFO] Chart.yaml: icon is recommended -[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler - -1 chart(s) linted, 0 chart(s) failed diff --git a/pkg/helm/cmd/helm/testdata/output/lint-quiet-with-error.txt b/pkg/helm/cmd/helm/testdata/output/lint-quiet-with-error.txt deleted file mode 100644 index e3d29a5a..00000000 --- a/pkg/helm/cmd/helm/testdata/output/lint-quiet-with-error.txt +++ /dev/null @@ -1,8 +0,0 @@ -==> Linting testdata/testcharts/chart-bad-requirements -[ERROR] Chart.yaml: unable to parse YAML - error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator -[ERROR] templates/: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator -[ERROR] : unable to load chart - cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator - -Error: 2 chart(s) linted, 1 chart(s) failed diff --git a/pkg/helm/cmd/helm/testdata/output/list-all.txt b/pkg/helm/cmd/helm/testdata/output/list-all.txt deleted file mode 100644 index ef6d44cd..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-all.txt +++ /dev/null @@ -1,9 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 -gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 -groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 -thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-date-reversed.txt b/pkg/helm/cmd/helm/testdata/output/list-date-reversed.txt deleted file mode 100644 index 8b4e71a3..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-date-reversed.txt +++ /dev/null @@ -1,5 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-date.txt b/pkg/helm/cmd/helm/testdata/output/list-date.txt deleted file mode 100644 index 3d2b27ad..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-date.txt +++ /dev/null @@ -1,5 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-failed.txt b/pkg/helm/cmd/helm/testdata/output/list-failed.txt deleted file mode 100644 index a8ec3e13..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-failed.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-filter.txt b/pkg/helm/cmd/helm/testdata/output/list-filter.txt deleted file mode 100644 index 0a820922..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-filter.txt +++ /dev/null @@ -1,5 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-max.txt b/pkg/helm/cmd/helm/testdata/output/list-max.txt deleted file mode 100644 index a909322b..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-max.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-namespace.txt b/pkg/helm/cmd/helm/testdata/output/list-namespace.txt deleted file mode 100644 index 9382327d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-namespace.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -starlord milano 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-no-headers.txt b/pkg/helm/cmd/helm/testdata/output/list-no-headers.txt deleted file mode 100644 index 9d11d0ca..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-no-headers.txt +++ /dev/null @@ -1,4 +0,0 @@ -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-offset.txt b/pkg/helm/cmd/helm/testdata/output/list-offset.txt deleted file mode 100644 index 36e963ca..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-offset.txt +++ /dev/null @@ -1,4 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-pending.txt b/pkg/helm/cmd/helm/testdata/output/list-pending.txt deleted file mode 100644 index f3d7aa03..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-pending.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-reverse.txt b/pkg/helm/cmd/helm/testdata/output/list-reverse.txt deleted file mode 100644 index da178b2c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-reverse.txt +++ /dev/null @@ -1,5 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-short-json.txt b/pkg/helm/cmd/helm/testdata/output/list-short-json.txt deleted file mode 100644 index acbf1e44..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-short-json.txt +++ /dev/null @@ -1 +0,0 @@ -["hummingbird","iguana","rocket","starlord"] diff --git a/pkg/helm/cmd/helm/testdata/output/list-short-yaml.txt b/pkg/helm/cmd/helm/testdata/output/list-short-yaml.txt deleted file mode 100644 index 86fb3d67..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-short-yaml.txt +++ /dev/null @@ -1,4 +0,0 @@ -- hummingbird -- iguana -- rocket -- starlord diff --git a/pkg/helm/cmd/helm/testdata/output/list-short.txt b/pkg/helm/cmd/helm/testdata/output/list-short.txt deleted file mode 100644 index 0a63be99..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-short.txt +++ /dev/null @@ -1,4 +0,0 @@ -hummingbird -iguana -rocket -starlord diff --git a/pkg/helm/cmd/helm/testdata/output/list-superseded.txt b/pkg/helm/cmd/helm/testdata/output/list-superseded.txt deleted file mode 100644 index 50b43587..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-superseded.txt +++ /dev/null @@ -1,3 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 -starlord default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-uninstalled.txt b/pkg/helm/cmd/helm/testdata/output/list-uninstalled.txt deleted file mode 100644 index 430cf32f..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-uninstalled.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list-uninstalling.txt b/pkg/helm/cmd/helm/testdata/output/list-uninstalling.txt deleted file mode 100644 index 92289639..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list-uninstalling.txt +++ /dev/null @@ -1,2 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/list.txt b/pkg/helm/cmd/helm/testdata/output/list.txt deleted file mode 100644 index 0a820922..00000000 --- a/pkg/helm/cmd/helm/testdata/output/list.txt +++ /dev/null @@ -1,5 +0,0 @@ -NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION -hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 -iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 -rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_args_comp.txt b/pkg/helm/cmd/helm/testdata/output/plugin_args_comp.txt deleted file mode 100644 index 4070cb1e..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_args_comp.txt +++ /dev/null @@ -1,6 +0,0 @@ -plugin.complete was called -Namespace: default -Num args received: 1 -Args received: -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_args_flag_comp.txt b/pkg/helm/cmd/helm/testdata/output/plugin_args_flag_comp.txt deleted file mode 100644 index 87300fa9..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_args_flag_comp.txt +++ /dev/null @@ -1,6 +0,0 @@ -plugin.complete was called -Namespace: default -Num args received: 2 -Args received: --myflag -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_args_many_args_comp.txt b/pkg/helm/cmd/helm/testdata/output/plugin_args_many_args_comp.txt deleted file mode 100644 index f3c386b6..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_args_many_args_comp.txt +++ /dev/null @@ -1,6 +0,0 @@ -plugin.complete was called -Namespace: mynamespace -Num args received: 2 -Args received: --myflag start -:2 -Completion ended with directive: ShellCompDirectiveNoSpace diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_args_ns_comp.txt b/pkg/helm/cmd/helm/testdata/output/plugin_args_ns_comp.txt deleted file mode 100644 index 13bfcd3f..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_args_ns_comp.txt +++ /dev/null @@ -1,6 +0,0 @@ -plugin.complete was called -Namespace: mynamespace -Num args received: 1 -Args received: -:2 -Completion ended with directive: ShellCompDirectiveNoSpace diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_echo_no_directive.txt b/pkg/helm/cmd/helm/testdata/output/plugin_echo_no_directive.txt deleted file mode 100644 index 99cc47c1..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_echo_no_directive.txt +++ /dev/null @@ -1,6 +0,0 @@ -echo plugin.complete was called -Namespace: mynamespace -Num args received: 1 -Args received: -:0 -Completion ended with directive: ShellCompDirectiveDefault diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_list_comp.txt b/pkg/helm/cmd/helm/testdata/output/plugin_list_comp.txt deleted file mode 100644 index 833efc5e..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_list_comp.txt +++ /dev/null @@ -1,7 +0,0 @@ -args echo args -echo echo stuff -env env stuff -exitwith exitwith code -fullenv show env vars -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/plugin_repeat_comp.txt b/pkg/helm/cmd/helm/testdata/output/plugin_repeat_comp.txt deleted file mode 100644 index 3fa05f0b..00000000 --- a/pkg/helm/cmd/helm/testdata/output/plugin_repeat_comp.txt +++ /dev/null @@ -1,6 +0,0 @@ -echo echo stuff -env env stuff -exitwith exitwith code -fullenv show env vars -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-comp.txt b/pkg/helm/cmd/helm/testdata/output/rollback-comp.txt deleted file mode 100644 index 2cfeed1f..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-comp.txt +++ /dev/null @@ -1,4 +0,0 @@ -carabins foo-0.1.0-beta.1 -> superseded -musketeers foo-0.1.0-beta.1 -> deployed -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-no-args.txt b/pkg/helm/cmd/helm/testdata/output/rollback-no-args.txt deleted file mode 100644 index a1bc30b7..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm rollback" requires at least 1 argument - -Usage: helm rollback [REVISION] [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-no-revision.txt b/pkg/helm/cmd/helm/testdata/output/rollback-no-revision.txt deleted file mode 100644 index ae3c6f1c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-no-revision.txt +++ /dev/null @@ -1 +0,0 @@ -Rollback was a success! Happy Helming! diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-non-existent-version.txt b/pkg/helm/cmd/helm/testdata/output/rollback-non-existent-version.txt deleted file mode 100644 index 9c2e10e1..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-non-existent-version.txt +++ /dev/null @@ -1 +0,0 @@ -Error: release has no 3 version diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-timeout.txt b/pkg/helm/cmd/helm/testdata/output/rollback-timeout.txt deleted file mode 100644 index ae3c6f1c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-timeout.txt +++ /dev/null @@ -1 +0,0 @@ -Rollback was a success! Happy Helming! diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-wait-for-jobs.txt b/pkg/helm/cmd/helm/testdata/output/rollback-wait-for-jobs.txt deleted file mode 100644 index ae3c6f1c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-wait-for-jobs.txt +++ /dev/null @@ -1 +0,0 @@ -Rollback was a success! Happy Helming! diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-wait.txt b/pkg/helm/cmd/helm/testdata/output/rollback-wait.txt deleted file mode 100644 index ae3c6f1c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-wait.txt +++ /dev/null @@ -1 +0,0 @@ -Rollback was a success! Happy Helming! diff --git a/pkg/helm/cmd/helm/testdata/output/rollback-wrong-args-comp.txt b/pkg/helm/cmd/helm/testdata/output/rollback-wrong-args-comp.txt deleted file mode 100644 index 8d9fad57..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback-wrong-args-comp.txt +++ /dev/null @@ -1,2 +0,0 @@ -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/rollback.txt b/pkg/helm/cmd/helm/testdata/output/rollback.txt deleted file mode 100644 index ae3c6f1c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/rollback.txt +++ /dev/null @@ -1 +0,0 @@ -Rollback was a success! Happy Helming! diff --git a/pkg/helm/cmd/helm/testdata/output/schema-negative-cli.txt b/pkg/helm/cmd/helm/testdata/output/schema-negative-cli.txt deleted file mode 100644 index c4a5cc51..00000000 --- a/pkg/helm/cmd/helm/testdata/output/schema-negative-cli.txt +++ /dev/null @@ -1,4 +0,0 @@ -Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): -empty: -- age: Must be greater than or equal to 0 - diff --git a/pkg/helm/cmd/helm/testdata/output/schema-negative.txt b/pkg/helm/cmd/helm/testdata/output/schema-negative.txt deleted file mode 100644 index 929af551..00000000 --- a/pkg/helm/cmd/helm/testdata/output/schema-negative.txt +++ /dev/null @@ -1,5 +0,0 @@ -Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): -empty: -- (root): employmentInfo is required -- age: Must be greater than or equal to 0 - diff --git a/pkg/helm/cmd/helm/testdata/output/schema.txt b/pkg/helm/cmd/helm/testdata/output/schema.txt deleted file mode 100644 index 60e4d026..00000000 --- a/pkg/helm/cmd/helm/testdata/output/schema.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: schema -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/status-with-resources.json b/pkg/helm/cmd/helm/testdata/output/status-with-resources.json deleted file mode 100644 index 275e0cfc..00000000 --- a/pkg/helm/cmd/helm/testdata/output/status-with-resources.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"} diff --git a/pkg/helm/cmd/helm/testdata/output/status-wrong-args-comp.txt b/pkg/helm/cmd/helm/testdata/output/status-wrong-args-comp.txt deleted file mode 100644 index 8d9fad57..00000000 --- a/pkg/helm/cmd/helm/testdata/output/status-wrong-args-comp.txt +++ /dev/null @@ -1,2 +0,0 @@ -:4 -Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/status.json b/pkg/helm/cmd/helm/testdata/output/status.json deleted file mode 100644 index 4b499c93..00000000 --- a/pkg/helm/cmd/helm/testdata/output/status.json +++ /dev/null @@ -1 +0,0 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/pkg/helm/cmd/helm/testdata/output/subchart-schema-cli-negative.txt b/pkg/helm/cmd/helm/testdata/output/subchart-schema-cli-negative.txt deleted file mode 100644 index 7396b4bf..00000000 --- a/pkg/helm/cmd/helm/testdata/output/subchart-schema-cli-negative.txt +++ /dev/null @@ -1,4 +0,0 @@ -Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): -subchart-with-schema: -- age: Must be greater than or equal to 0 - diff --git a/pkg/helm/cmd/helm/testdata/output/subchart-schema-cli.txt b/pkg/helm/cmd/helm/testdata/output/subchart-schema-cli.txt deleted file mode 100644 index 60e4d026..00000000 --- a/pkg/helm/cmd/helm/testdata/output/subchart-schema-cli.txt +++ /dev/null @@ -1,8 +0,0 @@ -NAME: schema -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 1 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/subchart-schema-negative.txt b/pkg/helm/cmd/helm/testdata/output/subchart-schema-negative.txt deleted file mode 100644 index 7b1f654a..00000000 --- a/pkg/helm/cmd/helm/testdata/output/subchart-schema-negative.txt +++ /dev/null @@ -1,6 +0,0 @@ -Error: INSTALLATION FAILED: values don't meet the specifications of the schema(s) in the following chart(s): -chart-without-schema: -- (root): lastname is required -subchart-with-schema: -- (root): age is required - diff --git a/pkg/helm/cmd/helm/testdata/output/template-chart-bad-type.txt b/pkg/helm/cmd/helm/testdata/output/template-chart-bad-type.txt deleted file mode 100644 index d8a3bf27..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-chart-bad-type.txt +++ /dev/null @@ -1 +0,0 @@ -Error: validation: chart.metadata.type must be application or library diff --git a/pkg/helm/cmd/helm/testdata/output/template-chart-with-template-lib-archive-dep.txt b/pkg/helm/cmd/helm/testdata/output/template-chart-with-template-lib-archive-dep.txt deleted file mode 100644 index c954b8e1..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-chart-with-template-lib-archive-dep.txt +++ /dev/null @@ -1,61 +0,0 @@ ---- -# Source: chart-with-template-lib-archive-dep/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - labels: - app: chart-with-template-lib-archive-dep - chart: chart-with-template-lib-archive-dep-0.1.0 - heritage: Helm - release: release-name - name: release-name-chart-with-template-lib-archive-dep -spec: - ports: - - name: http - port: 80 - targetPort: http - selector: - app: chart-with-template-lib-archive-dep - release: release-name - type: ClusterIP ---- -# Source: chart-with-template-lib-archive-dep/templates/deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: release-name-chart-with-template-lib-archive-dep - labels: - app: chart-with-template-lib-archive-dep - chart: chart-with-template-lib-archive-dep-0.1.0 - release: release-name - heritage: Helm -spec: - replicas: 1 - selector: - matchLabels: - app: chart-with-template-lib-archive-dep - release: release-name - template: - metadata: - labels: - app: chart-with-template-lib-archive-dep - release: release-name - spec: - containers: - - name: chart-with-template-lib-archive-dep - image: "nginx:stable" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 80 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http - resources: - {} diff --git a/pkg/helm/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt b/pkg/helm/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt deleted file mode 100644 index 74a2a2df..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-chart-with-template-lib-dep.txt +++ /dev/null @@ -1,61 +0,0 @@ ---- -# Source: chart-with-template-lib-dep/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - labels: - app: chart-with-template-lib-dep - chart: chart-with-template-lib-dep-0.1.0 - heritage: Helm - release: release-name - name: release-name-chart-with-template-lib-dep -spec: - ports: - - name: http - port: 80 - targetPort: http - selector: - app: chart-with-template-lib-dep - release: release-name - type: ClusterIP ---- -# Source: chart-with-template-lib-dep/templates/deployment.yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: release-name-chart-with-template-lib-dep - labels: - app: chart-with-template-lib-dep - chart: chart-with-template-lib-dep-0.1.0 - release: release-name - heritage: Helm -spec: - replicas: 1 - selector: - matchLabels: - app: chart-with-template-lib-dep - release: release-name - template: - metadata: - labels: - app: chart-with-template-lib-dep - release: release-name - spec: - containers: - - name: chart-with-template-lib-dep - image: "nginx:stable" - imagePullPolicy: IfNotPresent - ports: - - name: http - containerPort: 80 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http - resources: - {} diff --git a/pkg/helm/cmd/helm/testdata/output/template-lib-chart.txt b/pkg/helm/cmd/helm/testdata/output/template-lib-chart.txt deleted file mode 100644 index d8a3bf27..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-lib-chart.txt +++ /dev/null @@ -1 +0,0 @@ -Error: validation: chart.metadata.type must be application or library diff --git a/pkg/helm/cmd/helm/testdata/output/template-name-template.txt b/pkg/helm/cmd/helm/testdata/output/template-name-template.txt deleted file mode 100644 index 9406048d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-name-template.txt +++ /dev/null @@ -1,114 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "foobar-ywjj-baz" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "foobar-ywjj-baz-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "foobar-ywjj-baz-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "foobar-ywjj-baz-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-no-args.txt b/pkg/helm/cmd/helm/testdata/output/template-no-args.txt deleted file mode 100644 index f72f2b8c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm template" requires at least 1 argument - -Usage: helm template [NAME] [CHART] [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/template-set.txt b/pkg/helm/cmd/helm/testdata/output/template-set.txt deleted file mode 100644 index 4040991c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-set.txt +++ /dev/null @@ -1,114 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-show-only-glob.txt b/pkg/helm/cmd/helm/testdata/output/template-show-only-glob.txt deleted file mode 100644 index b2d2b1c2..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-show-only-glob.txt +++ /dev/null @@ -1,24 +0,0 @@ ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default diff --git a/pkg/helm/cmd/helm/testdata/output/template-show-only-multiple.txt b/pkg/helm/cmd/helm/testdata/output/template-show-only-multiple.txt deleted file mode 100644 index 1aac3081..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-show-only-multiple.txt +++ /dev/null @@ -1,38 +0,0 @@ ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta diff --git a/pkg/helm/cmd/helm/testdata/output/template-show-only-one.txt b/pkg/helm/cmd/helm/testdata/output/template-show-only-one.txt deleted file mode 100644 index 9cc34f51..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-show-only-one.txt +++ /dev/null @@ -1,21 +0,0 @@ ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart diff --git a/pkg/helm/cmd/helm/testdata/output/template-skip-tests.txt b/pkg/helm/cmd/helm/testdata/output/template-skip-tests.txt deleted file mode 100644 index 5c907b56..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-skip-tests.txt +++ /dev/null @@ -1,85 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart diff --git a/pkg/helm/cmd/helm/testdata/output/template-subchart-cm-set-file.txt b/pkg/helm/cmd/helm/testdata/output/template-subchart-cm-set-file.txt deleted file mode 100644 index 56844e29..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-subchart-cm-set-file.txt +++ /dev/null @@ -1,122 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: subchart-cm -data: - value: qux ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-subchart-cm-set.txt b/pkg/helm/cmd/helm/testdata/output/template-subchart-cm-set.txt deleted file mode 100644 index e52f7c23..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-subchart-cm-set.txt +++ /dev/null @@ -1,122 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: subchart-cm -data: - value: baz ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-subchart-cm.txt b/pkg/helm/cmd/helm/testdata/output/template-subchart-cm.txt deleted file mode 100644 index 9cc9e229..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-subchart-cm.txt +++ /dev/null @@ -1,122 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/configmap.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: subchart-cm -data: - value: foo ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-values-files.txt b/pkg/helm/cmd/helm/testdata/output/template-values-files.txt deleted file mode 100644 index 4040991c..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-values-files.txt +++ /dev/null @@ -1,114 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-with-api-version.txt b/pkg/helm/cmd/helm/testdata/output/template-with-api-version.txt deleted file mode 100644 index 7e1c3500..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-with-api-version.txt +++ /dev/null @@ -1,115 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" - kube-api-version/test: v1 -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-with-crds.txt b/pkg/helm/cmd/helm/testdata/output/template-with-crds.txt deleted file mode 100644 index 256fc7c3..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-with-crds.txt +++ /dev/null @@ -1,131 +0,0 @@ ---- -# Source: subchart/crds/crdA.yaml -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - name: testcrds.testcrdgroups.example.com -spec: - group: testcrdgroups.example.com - version: v1alpha1 - names: - kind: TestCRD - listKind: TestCRDList - plural: testcrds - shortNames: - - tc - singular: authconfig - ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt b/pkg/helm/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt deleted file mode 100644 index 909c543d..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt +++ /dev/null @@ -1,13 +0,0 @@ ---- -# Source: chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-my-alpine" -spec: - containers: - - name: waiter - image: "alpine:3.9" - command: ["/bin/sleep","9000"] -invalid -Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':' diff --git a/pkg/helm/cmd/helm/testdata/output/template-with-invalid-yaml.txt b/pkg/helm/cmd/helm/testdata/output/template-with-invalid-yaml.txt deleted file mode 100644 index 687227b9..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-with-invalid-yaml.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':' - -Use --debug flag to render out invalid YAML diff --git a/pkg/helm/cmd/helm/testdata/output/template-with-kube-version.txt b/pkg/helm/cmd/helm/testdata/output/template-with-kube-version.txt deleted file mode 100644 index 9d326f32..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template-with-kube-version.txt +++ /dev/null @@ -1,114 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "16" - kube-version/version: "v1.16.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/template.txt b/pkg/helm/cmd/helm/testdata/output/template.txt deleted file mode 100644 index 58c480b4..00000000 --- a/pkg/helm/cmd/helm/testdata/output/template.txt +++ /dev/null @@ -1,114 +0,0 @@ ---- -# Source: subchart/templates/subdir/serviceaccount.yaml -apiVersion: v1 -kind: ServiceAccount -metadata: - name: subchart-sa ---- -# Source: subchart/templates/subdir/role.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: subchart-role -rules: -- apiGroups: [""] - resources: ["pods"] - verbs: ["get","list","watch"] ---- -# Source: subchart/templates/subdir/rolebinding.yaml -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: subchart-binding -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: subchart-role -subjects: -- kind: ServiceAccount - name: subchart-sa - namespace: default ---- -# Source: subchart/charts/subcharta/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subcharta - labels: - helm.sh/chart: "subcharta-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: apache - selector: - app.kubernetes.io/name: subcharta ---- -# Source: subchart/charts/subchartb/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchartb - labels: - helm.sh/chart: "subchartb-0.1.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchartb ---- -# Source: subchart/templates/service.yaml -apiVersion: v1 -kind: Service -metadata: - name: subchart - labels: - helm.sh/chart: "subchart-0.1.0" - app.kubernetes.io/instance: "release-name" - kube-version/major: "1" - kube-version/minor: "20" - kube-version/version: "v1.20.0" -spec: - type: ClusterIP - ports: - - port: 80 - targetPort: 80 - protocol: TCP - name: nginx - selector: - app.kubernetes.io/name: subchart ---- -# Source: subchart/templates/tests/test-config.yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: "release-name-testconfig" - annotations: - "helm.sh/hook": test -data: - message: Hello World ---- -# Source: subchart/templates/tests/test-nothing.yaml -apiVersion: v1 -kind: Pod -metadata: - name: "release-name-test" - annotations: - "helm.sh/hook": test -spec: - containers: - - name: test - image: "alpine:latest" - envFrom: - - configMapRef: - name: "release-name-testconfig" - command: - - echo - - "$message" - restartPolicy: Never diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall-keep-history.txt b/pkg/helm/cmd/helm/testdata/output/uninstall-keep-history.txt deleted file mode 100644 index f5454b88..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall-keep-history.txt +++ /dev/null @@ -1 +0,0 @@ -release "aeneas" uninstalled diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall-multiple.txt b/pkg/helm/cmd/helm/testdata/output/uninstall-multiple.txt deleted file mode 100644 index ee1c67d2..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall-multiple.txt +++ /dev/null @@ -1,2 +0,0 @@ -release "aeneas" uninstalled -release "aeneas2" uninstalled diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall-no-args.txt b/pkg/helm/cmd/helm/testdata/output/uninstall-no-args.txt deleted file mode 100644 index fc01a75b..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall-no-args.txt +++ /dev/null @@ -1,3 +0,0 @@ -Error: "helm uninstall" requires at least 1 argument - -Usage: helm uninstall RELEASE_NAME [...] [flags] diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall-no-hooks.txt b/pkg/helm/cmd/helm/testdata/output/uninstall-no-hooks.txt deleted file mode 100644 index f5454b88..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall-no-hooks.txt +++ /dev/null @@ -1 +0,0 @@ -release "aeneas" uninstalled diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall-timeout.txt b/pkg/helm/cmd/helm/testdata/output/uninstall-timeout.txt deleted file mode 100644 index f5454b88..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall-timeout.txt +++ /dev/null @@ -1 +0,0 @@ -release "aeneas" uninstalled diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall-wait.txt b/pkg/helm/cmd/helm/testdata/output/uninstall-wait.txt deleted file mode 100644 index f5454b88..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall-wait.txt +++ /dev/null @@ -1 +0,0 @@ -release "aeneas" uninstalled diff --git a/pkg/helm/cmd/helm/testdata/output/uninstall.txt b/pkg/helm/cmd/helm/testdata/output/uninstall.txt deleted file mode 100644 index f5454b88..00000000 --- a/pkg/helm/cmd/helm/testdata/output/uninstall.txt +++ /dev/null @@ -1 +0,0 @@ -release "aeneas" uninstalled diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt deleted file mode 100644 index 6dddc734..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-bad-dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -Error: cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt deleted file mode 100644 index 8f24574a..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-bad-or-missing-existing-release.txt +++ /dev/null @@ -1 +0,0 @@ -Error: UPGRADE FAILED: "funny-bunny" has no deployed releases diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-dependency-update.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-dependency-update.txt deleted file mode 100644 index 73174063..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-dependency-update.txt +++ /dev/null @@ -1,11 +0,0 @@ -Release "funny-bunny" has been upgraded. Happy Helming! -NAME: funny-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 3 -TEST SUITE: None -NOTES: -PARENT NOTES diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-install-timeout.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-install-timeout.txt deleted file mode 100644 index d428c2eb..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-install-timeout.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "crazy-bunny" has been upgraded. Happy Helming! -NAME: crazy-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 2 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-install.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-install.txt deleted file mode 100644 index 2fc53807..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-install.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "zany-bunny" has been upgraded. Happy Helming! -NAME: zany-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 2 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt deleted file mode 100644 index adf2ae89..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-missing-dependencies.txt +++ /dev/null @@ -1 +0,0 @@ -Error: An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: found in Chart.yaml, but missing in charts/ directory: reqsubchart2 diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-pending-install.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-pending-install.txt deleted file mode 100644 index 57a8e787..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-pending-install.txt +++ /dev/null @@ -1 +0,0 @@ -Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-reset-values.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-reset-values.txt deleted file mode 100644 index 16b1e624..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-reset-values.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "funny-bunny" has been upgraded. Happy Helming! -NAME: funny-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 5 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-reset-values2.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-reset-values2.txt deleted file mode 100644 index 68f7fc03..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-reset-values2.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "funny-bunny" has been upgraded. Happy Helming! -NAME: funny-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 6 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-timeout.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-timeout.txt deleted file mode 100644 index 68eb7f05..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-timeout.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "funny-bunny" has been upgraded. Happy Helming! -NAME: funny-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 4 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-wait-for-jobs.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-wait-for-jobs.txt deleted file mode 100644 index bf453ceb..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-wait-for-jobs.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "crazy-bunny" has been upgraded. Happy Helming! -NAME: crazy-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 3 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade-with-wait.txt b/pkg/helm/cmd/helm/testdata/output/upgrade-with-wait.txt deleted file mode 100644 index bf453ceb..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade-with-wait.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "crazy-bunny" has been upgraded. Happy Helming! -NAME: crazy-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 3 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/upgrade.txt b/pkg/helm/cmd/helm/testdata/output/upgrade.txt deleted file mode 100644 index 68ec3dc3..00000000 --- a/pkg/helm/cmd/helm/testdata/output/upgrade.txt +++ /dev/null @@ -1,9 +0,0 @@ -Release "funny-bunny" has been upgraded. Happy Helming! -NAME: funny-bunny -LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 -NAMESPACE: default -STATUS: deployed -REVISION: 3 -TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/version-client-shorthand.txt b/pkg/helm/cmd/helm/testdata/output/version-client-shorthand.txt deleted file mode 100644 index e204f7a4..00000000 --- a/pkg/helm/cmd/helm/testdata/output/version-client-shorthand.txt +++ /dev/null @@ -1 +0,0 @@ -version.BuildInfo{Version:"v3.14", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/pkg/helm/cmd/helm/testdata/output/version-client.txt b/pkg/helm/cmd/helm/testdata/output/version-client.txt deleted file mode 100644 index e204f7a4..00000000 --- a/pkg/helm/cmd/helm/testdata/output/version-client.txt +++ /dev/null @@ -1 +0,0 @@ -version.BuildInfo{Version:"v3.14", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/pkg/helm/cmd/helm/testdata/output/version-short.txt b/pkg/helm/cmd/helm/testdata/output/version-short.txt deleted file mode 100644 index 3ef02b86..00000000 --- a/pkg/helm/cmd/helm/testdata/output/version-short.txt +++ /dev/null @@ -1 +0,0 @@ -v3.14 diff --git a/pkg/helm/cmd/helm/testdata/output/version-template.txt b/pkg/helm/cmd/helm/testdata/output/version-template.txt deleted file mode 100644 index d33c5a92..00000000 --- a/pkg/helm/cmd/helm/testdata/output/version-template.txt +++ /dev/null @@ -1 +0,0 @@ -Version: v3.14 \ No newline at end of file diff --git a/pkg/helm/cmd/helm/testdata/output/version.txt b/pkg/helm/cmd/helm/testdata/output/version.txt deleted file mode 100644 index e204f7a4..00000000 --- a/pkg/helm/cmd/helm/testdata/output/version.txt +++ /dev/null @@ -1 +0,0 @@ -version.BuildInfo{Version:"v3.14", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/pkg/helm/cmd/helm/testdata/plugins.yaml b/pkg/helm/cmd/helm/testdata/plugins.yaml deleted file mode 100644 index 69086973..00000000 --- a/pkg/helm/cmd/helm/testdata/plugins.yaml +++ /dev/null @@ -1,3 +0,0 @@ -plugins: -- name: testplugin - url: testdata/testplugin diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md b/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md deleted file mode 100755 index 0e06414d..00000000 --- a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md +++ /dev/null @@ -1,831 +0,0 @@ -# Common: The Helm Helper Chart - -This chart is designed to make it easier for you to build and maintain Helm -charts. - -It provides utilities that reflect best practices of Kubernetes chart development, -making it faster for you to write charts. - -## Tips - -A few tips for working with Common: - -- Be careful when using functions that generate random data (like `common.fullname.unique`). - They may trigger unwanted upgrades or have other side effects. - -In this document, we use `release-name` as the name of the release. - -## Resource Kinds - -Kubernetes defines a variety of resource kinds, from `Secret` to `StatefulSet`. -We define some of the most common kinds in a way that lets you easily work with -them. - -The resource kind templates are designed to make it much faster for you to -define _basic_ versions of these resources. They allow you to extend and modify -just what you need, without having to copy around lots of boilerplate. - -To make use of these templates you must define a template that will extend the -base template (though it can be empty). The name of this template is then passed -to the base template, for example: - -```yaml -{{- template "common.service" (list . "mychart.service") -}} -{{- define "mychart.service" -}} -## Define overrides for your Service resource here, e.g. -# metadata: -# labels: -# custom: label -# spec: -# ports: -# - port: 8080 -{{- end -}} -``` - -Note that the `common.service` template defines two parameters: - - - The root context (usually `.`) - - A template name containing the service definition overrides - -A limitation of the Go template library is that a template can only take a -single argument. The `list` function is used to workaround this by constructing -a list or array of arguments that is passed to the template. - -The `common.service` template is responsible for rendering the templates with -the root context and merging any overrides. As you can see, this makes it very -easy to create a basic `Service` resource without having to copy around the -standard metadata and labels. - -Each implemented base resource is described in greater detail below. - -### `common.service` - -The `common.service` template creates a basic `Service` resource with the -following defaults: - -- Service type (ClusterIP, NodePort, LoadBalancer) made configurable by `.Values.service.type` -- Named port `http` configured on port 80 -- Selector set to `app: {{ template "common.name" }}, release: {{ .Release.Name | quote }}` to match the default used in the `Deployment` resource - -Example template: - -```yaml -{{- template "common.service" (list . "mychart.mail.service") -}} -{{- define "mychart.mail.service" -}} -metadata: - name: {{ template "common.fullname" . }}-mail # overrides the default name to add a suffix - labels: # appended to the labels section - protocol: mail -spec: - ports: # composes the `ports` section of the service definition. - - name: smtp - port: 25 - targetPort: 25 - - name: imaps - port: 993 - targetPort: 993 - selector: # this is appended to the default selector - protocol: mail -{{- end -}} ---- -{{ template "common.service" (list . "mychart.web.service") -}} -{{- define "mychart.web.service" -}} -metadata: - name: {{ template "common.fullname" . }}-www # overrides the default name to add a suffix - labels: # appended to the labels section - protocol: www -spec: - ports: # composes the `ports` section of the service definition. - - name: www - port: 80 - targetPort: 8080 -{{- end -}} -``` - -The above template defines _two_ services: a web service and a mail service. - -The most important part of a service definition is the `ports` object, which -defines the ports that this service will listen on. Most of the time, -`selector` is computed for you. But you can replace it or add to it. - -The output of the example above is: - -```yaml -apiVersion: v1 -kind: Service -metadata: - labels: - app: service - chart: service-0.1.0 - heritage: Tiller - protocol: mail - release: release-name - name: release-name-service-mail -spec: - ports: - - name: smtp - port: 25 - targetPort: 25 - - name: imaps - port: 993 - targetPort: 993 - selector: - app: service - release: release-name - protocol: mail - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app: service - chart: service-0.1.0 - heritage: Tiller - protocol: www - release: release-name - name: release-name-service-www -spec: - ports: - - name: www - port: 80 - targetPort: 8080 - type: ClusterIP -``` - -## `common.deployment` - -The `common.deployment` template defines a basic `Deployment`. Underneath the -hood, it uses `common.container` (see next section). - -By default, the pod template within the deployment defines the labels `app: {{ template "common.name" . }}` -and `release: {{ .Release.Name | quote }` as this is also used as the selector. The -standard set of labels are not used as some of these can change during upgrades, -which causes the replica sets and pods to not correctly match. - -Example use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -## Define overrides for your Deployment resource here, e.g. -spec: - replicas: {{ .Values.replicaCount }} -{{- end -}} -``` - -## `common.container` - -The `common.container` template creates a basic `Container` spec to be used -within a `Deployment` or `ReplicaSet`. It holds the following defaults: - -- The name is set to the chart name -- Uses `.Values.image` to describe the image to run, with the following spec: - ```yaml - image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent - ``` -- Exposes the named port `http` as port 80 -- Lays out the compute resources using `.Values.resources` - -Example use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -## Define overrides for your Deployment resource here, e.g. -spec: - template: - spec: - containers: - - {{ template "common.container" (list . "mychart.deployment.container") }} -{{- end -}} -{{- define "mychart.deployment.container" -}} -## Define overrides for your Container here, e.g. -livenessProbe: - httpGet: - path: / - port: 80 -readinessProbe: - httpGet: - path: / - port: 80 -{{- end -}} -``` - -The above example creates a `Deployment` resource which makes use of the -`common.container` template to populate the PodSpec's container list. The usage -of this template is similar to the other resources, you must define and -reference a template that contains overrides for the container object. - -The most important part of a container definition is the image you want to run. -As mentioned above, this is derived from `.Values.image` by default. It is a -best practice to define the image, tag and pull policy in your charts' values as -this makes it easy for an operator to change the image registry, or use a -specific tag or version. Another example of configuration that should be exposed -to chart operators is the container's required compute resources, as this is -also very specific to an operators environment. An example `values.yaml` for -your chart could look like: - -```yaml -image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent -resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi -``` - -The output of running the above values through the earlier template is: - -```yaml -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - labels: - app: deployment - chart: deployment-0.1.0 - heritage: Tiller - release: release-name - name: release-name-deployment -spec: - template: - metadata: - labels: - app: deployment - spec: - containers: - - image: nginx:stable - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: 80 - name: deployment - ports: - - containerPort: 80 - name: http - readinessProbe: - httpGet: - path: / - port: 80 - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi -``` - -## `common.configmap` - -The `common.configmap` template creates an empty `ConfigMap` resource that you -can override with your configuration. - -Example use: - -```yaml -{{- template "common.configmap" (list . "mychart.configmap") -}} -{{- define "mychart.configmap" -}} -data: - zeus: cat - athena: cat - julius: cat - one: |- - {{ .Files.Get "file1.txt" }} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: v1 -data: - athena: cat - julius: cat - one: This is a file. - zeus: cat -kind: ConfigMap -metadata: - labels: - app: configmap - chart: configmap-0.1.0 - heritage: Tiller - release: release-name - name: release-name-configmap -``` - -## `common.secret` - -The `common.secret` template creates an empty `Secret` resource that you -can override with your secrets. - -Example use: - -```yaml -{{- template "common.secret" (list . "mychart.secret") -}} -{{- define "mychart.secret" -}} -data: - zeus: {{ print "cat" | b64enc }} - athena: {{ print "cat" | b64enc }} - julius: {{ print "cat" | b64enc }} - one: |- - {{ .Files.Get "file1.txt" | b64enc }} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: v1 -data: - athena: Y2F0 - julius: Y2F0 - one: VGhpcyBpcyBhIGZpbGUuCg== - zeus: Y2F0 -kind: Secret -metadata: - labels: - app: secret - chart: secret-0.1.0 - heritage: Tiller - release: release-name - name: release-name-secret -type: Opaque -``` - -## `common.ingress` - -The `common.ingress` template is designed to give you a well-defined `Ingress` -resource, that can be configured using `.Values.ingress`. An example values file -that can be used to configure the `Ingress` resource is: - -```yaml -ingress: - hosts: - - chart-example.local - annotations: - kubernetes.io/ingress.class: nginx - kubernetes.io/tls-acme: "true" - tls: - - secretName: chart-example-tls - hosts: - - chart-example.local -``` - -Example use: - -```yaml -{{- template "common.ingress" (list . "mychart.ingress") -}} -{{- define "mychart.ingress" -}} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - annotations: - kubernetes.io/ingress.class: nginx - kubernetes.io/tls-acme: "true" - labels: - app: ingress - chart: ingress-0.1.0 - heritage: Tiller - release: release-name - name: release-name-ingress -spec: - rules: - - host: chart-example.local - http: - paths: - - backend: - serviceName: release-name-ingress - servicePort: 80 - path: / - tls: - - hosts: - - chart-example.local - secretName: chart-example-tls -``` - -## `common.persistentvolumeclaim` - -`common.persistentvolumeclaim` can be used to easily add a -`PersistentVolumeClaim` resource to your chart that can be configured using -`.Values.persistence`: - -| Value | Description | -| ------------------------- | ------------------------------------------------------------------------------------------------------- | -| persistence.enabled | Whether or not to claim a persistent volume. If false, `common.volume.pvc` will use an emptyDir instead | -| persistence.storageClass | `StorageClass` name | -| persistence.accessMode | Access mode for persistent volume | -| persistence.size | Size of persistent volume | -| persistence.existingClaim | If defined, `PersistentVolumeClaim` is not created and `common.volume.pvc` helper uses this claim | - -An example values file that can be used to configure the -`PersistentVolumeClaim` resource is: - -```yaml -persistence: - enabled: true - storageClass: fast - accessMode: ReadWriteOnce - size: 8Gi -``` - -Example use: - -```yaml -{{- template "common.persistentvolumeclaim" (list . "mychart.persistentvolumeclaim") -}} -{{- define "mychart.persistentvolumeclaim" -}} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - app: persistentvolumeclaim - chart: persistentvolumeclaim-0.1.0 - heritage: Tiller - release: release-name - name: release-name-persistentvolumeclaim -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 8Gi - storageClassName: "fast" -``` - -## Partial API Objects - -When writing Kubernetes resources, you may find the following helpers useful to -construct parts of the spec. - -### EnvVar - -Use the EnvVar helpers within a container spec to simplify specifying key-value -environment variables or referencing secrets as values. - -Example Use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -spec: - template: - spec: - containers: - - {{ template "common.container" (list . "mychart.deployment.container") }} -{{- end -}} -{{- define "mychart.deployment.container" -}} -{{- $fullname := include "common.fullname" . -}} -env: -- {{ template "common.envvar.value" (list "ZEUS" "cat") }} -- {{ template "common.envvar.secret" (list "ATHENA" "secret-name" "athena") }} -{{- end -}} -``` - -Output: - -```yaml -... - spec: - containers: - - env: - - name: ZEUS - value: cat - - name: ATHENA - valueFrom: - secretKeyRef: - key: athena - name: secret-name -... -``` - -### Volume - -Use the Volume helpers within a `Deployment` spec to help define ConfigMap and -PersistentVolumeClaim volumes. - -Example Use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -spec: - template: - spec: - volumes: - - {{ template "common.volume.configMap" (list "config" "configmap-name") }} - - {{ template "common.volume.pvc" (list "data" "pvc-name" .Values.persistence) }} -{{- end -}} -``` - -Output: - -```yaml -... - spec: - volumes: - - configMap: - name: configmap-name - name: config - - name: data - persistentVolumeClaim: - claimName: pvc-name -... -``` - -The `common.volume.pvc` helper uses the following configuration from the `.Values.persistence` object: - -| Value | Description | -| ------------------------- | ----------------------------------------------------- | -| persistence.enabled | If false, creates an `emptyDir` instead | -| persistence.existingClaim | If set, uses this instead of the passed in claim name | - -## Utilities - -### `common.fullname` - -The `common.fullname` template generates a name suitable for the `name:` field -in Kubernetes metadata. It is used like this: - -```yaml -name: {{ template "common.fullname" . }} -``` - -The following different values can influence it: - -```yaml -# By default, fullname uses '{{ .Release.Name }}-{{ .Chart.Name }}'. This -# overrides that and uses the given string instead. -fullnameOverride: "some-name" - -# This adds a prefix -fullnamePrefix: "pre-" -# This appends a suffix -fullnameSuffix: "-suf" - -# Global versions of the above -global: - fullnamePrefix: "pp-" - fullnameSuffix: "-ps" -``` - -Example output: - -```yaml ---- -# with the values above -name: pp-pre-some-name-suf-ps - ---- -# the default, for release "happy-panda" and chart "wordpress" -name: happy-panda-wordpress -``` - -Output of this function is truncated at 54 characters, which leaves 9 additional -characters for customized overriding. Thus you can easily extend this name -in your own charts: - -```yaml -{{- define "my.fullname" -}} - {{ template "common.fullname" . }}-my-stuff -{{- end -}} -``` - -### `common.fullname.unique` - -The `common.fullname.unique` variant of fullname appends a unique seven-character -sequence to the end of the common name field. - -This takes all of the same parameters as `common.fullname` - -Example template: - -```yaml -uniqueName: {{ template "common.fullname.unique" . }} -``` - -Example output: - -```yaml -uniqueName: release-name-fullname-jl0dbwx -``` - -It is also impacted by the prefix and suffix definitions, as well as by -`.Values.fullnameOverride` - -Note that the effective maximum length of this function is 63 characters, not 54. - -### `common.name` - -The `common.name` template generates a name suitable for the `app` label. It is used like this: - -```yaml -app: {{ template "common.name" . }} -``` - -The following different values can influence it: - -```yaml -# By default, name uses '{{ .Chart.Name }}'. This -# overrides that and uses the given string instead. -nameOverride: "some-name" - -# This adds a prefix -namePrefix: "pre-" -# This appends a suffix -nameSuffix: "-suf" - -# Global versions of the above -global: - namePrefix: "pp-" - nameSuffix: "-ps" -``` - -Example output: - -```yaml ---- -# with the values above -name: pp-pre-some-name-suf-ps - ---- -# the default, for chart "wordpress" -name: wordpress -``` - -Output of this function is truncated at 54 characters, which leaves 9 additional -characters for customized overriding. Thus you can easily extend this name -in your own charts: - -```yaml -{{- define "my.name" -}} - {{ template "common.name" . }}-my-stuff -{{- end -}} -``` - -### `common.metadata` - -The `common.metadata` helper generates the `metadata:` section of a Kubernetes -resource. - -This takes three objects: - - .top: top context - - .fullnameOverride: override the fullname with this name - - .metadata - - .labels: key/value list of labels - - .annotations: key/value list of annotations - - .hook: name(s) of hook(s) - -It generates standard labels, annotations, hooks, and a name field. - -Example template: - -```yaml -{{ template "common.metadata" (dict "top" . "metadata" .Values.bio) }} ---- -{{ template "common.metadata" (dict "top" . "metadata" .Values.pet "fullnameOverride" .Values.pet.fullnameOverride) }} -``` - -Example values: - -```yaml -bio: - name: example - labels: - first: matt - last: butcher - nick: technosophos - annotations: - format: bio - destination: archive - hook: pre-install - -pet: - fullnameOverride: Zeus - -``` - -Example output: - -```yaml -metadata: - name: release-name-metadata - labels: - app: metadata - heritage: "Tiller" - release: "release-name" - chart: metadata-0.1.0 - first: "matt" - last: "butcher" - nick: "technosophos" - annotations: - "destination": "archive" - "format": "bio" - "helm.sh/hook": "pre-install" ---- -metadata: - name: Zeus - labels: - app: metadata - heritage: "Tiller" - release: "release-name" - chart: metadata-0.1.0 - annotations: -``` - -Most of the common templates that define a resource type (e.g. `common.configmap` -or `common.job`) use this to generate the metadata, which means they inherit -the same `labels`, `annotations`, `nameOverride`, and `hook` fields. - -### `common.labelize` - -`common.labelize` turns a map into a set of labels. - -Example template: - -```yaml -{{- $map := dict "first" "1" "second" "2" "third" "3" -}} -{{- template "common.labelize" $map -}} -``` - -Example output: - -```yaml -first: "1" -second: "2" -third: "3" -``` - -### `common.labels.standard` - -`common.labels.standard` prints the standard set of labels. - -Example usage: - -``` -{{ template "common.labels.standard" . }} -``` - -Example output: - -```yaml -app: labelizer -heritage: "Tiller" -release: "release-name" -chart: labelizer-0.1.0 -``` - -### `common.hook` - -The `common.hook` template is a convenience for defining hooks. - -Example template: - -```yaml -{{ template "common.hook" "pre-install,post-install" }} -``` - -Example output: - -```yaml -"helm.sh/hook": "pre-install,post-install" -``` - -### `common.chartref` - -The `common.chartref` helper prints the chart name and version, escaped to be -legal in a Kubernetes label field. - -Example template: - -```yaml -chartref: {{ template "common.chartref" . }} -``` - -For the chart `foo` with version `1.2.3-beta.55+1234`, this will render: - -```yaml -chartref: foo-1.2.3-beta.55_1234 -``` - -(Note that `+` is an illegal character in label values) diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/README.md b/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/README.md deleted file mode 100644 index 87b753f2..00000000 --- a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/README.md +++ /dev/null @@ -1,831 +0,0 @@ -# Common: The Helm Helper Chart - -This chart is designed to make it easier for you to build and maintain Helm -charts. - -It provides utilities that reflect best practices of Kubernetes chart development, -making it faster for you to write charts. - -## Tips - -A few tips for working with Common: - -- Be careful when using functions that generate random data (like `common.fullname.unique`). - They may trigger unwanted upgrades or have other side effects. - -In this document, we use `release-name` as the name of the release. - -## Resource Kinds - -Kubernetes defines a variety of resource kinds, from `Secret` to `StatefulSet`. -We define some of the most common kinds in a way that lets you easily work with -them. - -The resource kind templates are designed to make it much faster for you to -define _basic_ versions of these resources. They allow you to extend and modify -just what you need, without having to copy around lots of boilerplate. - -To make use of these templates you must define a template that will extend the -base template (though it can be empty). The name of this template is then passed -to the base template, for example: - -```yaml -{{- template "common.service" (list . "mychart.service") -}} -{{- define "mychart.service" -}} -## Define overrides for your Service resource here, e.g. -# metadata: -# labels: -# custom: label -# spec: -# ports: -# - port: 8080 -{{- end -}} -``` - -Note that the `common.service` template defines two parameters: - - - The root context (usually `.`) - - A template name containing the service definition overrides - -A limitation of the Go template library is that a template can only take a -single argument. The `list` function is used to workaround this by constructing -a list or array of arguments that is passed to the template. - -The `common.service` template is responsible for rendering the templates with -the root context and merging any overrides. As you can see, this makes it very -easy to create a basic `Service` resource without having to copy around the -standard metadata and labels. - -Each implemented base resource is described in greater detail below. - -### `common.service` - -The `common.service` template creates a basic `Service` resource with the -following defaults: - -- Service type (ClusterIP, NodePort, LoadBalancer) made configurable by `.Values.service.type` -- Named port `http` configured on port 80 -- Selector set to `app.kubernetes.io/name: {{ template "common.name" }}, app.kubernetes.io/instance: {{ .Release.Name | quote }}` to match the default used in the `Deployment` resource - -Example template: - -```yaml -{{- template "common.service" (list . "mychart.mail.service") -}} -{{- define "mychart.mail.service" -}} -metadata: - name: {{ template "common.fullname" . }}-mail # overrides the default name to add a suffix - labels: # appended to the labels section - protocol: mail -spec: - ports: # composes the `ports` section of the service definition. - - name: smtp - port: 25 - targetPort: 25 - - name: imaps - port: 993 - targetPort: 993 - selector: # this is appended to the default selector - protocol: mail -{{- end -}} ---- -{{ template "common.service" (list . "mychart.web.service") -}} -{{- define "mychart.web.service" -}} -metadata: - name: {{ template "common.fullname" . }}-www # overrides the default name to add a suffix - labels: # appended to the labels section - protocol: www -spec: - ports: # composes the `ports` section of the service definition. - - name: www - port: 80 - targetPort: 8080 -{{- end -}} -``` - -The above template defines _two_ services: a web service and a mail service. - -The most important part of a service definition is the `ports` object, which -defines the ports that this service will listen on. Most of the time, -`selector` is computed for you. But you can replace it or add to it. - -The output of the example above is: - -```yaml -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/name: service - helm.sh/chart: service-0.1.0 - app.kubernetes.io/managed-by: Helm - protocol: mail - app.kubernetes.io/instance: release-name - name: release-name-service-mail -spec: - ports: - - name: smtp - port: 25 - targetPort: 25 - - name: imaps - port: 993 - targetPort: 993 - selector: - app.kubernetes.io/name: service - app.kubernetes.io/instance: release-name - protocol: mail - type: ClusterIP ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/name: service - helm.sh/chart: service-0.1.0 - app.kubernetes.io/managed-by: Helm - protocol: www - app.kubernetes.io/instance: release-name - name: release-name-service-www -spec: - ports: - - name: www - port: 80 - targetPort: 8080 - type: ClusterIP -``` - -## `common.deployment` - -The `common.deployment` template defines a basic `Deployment`. Underneath the -hood, it uses `common.container` (see next section). - -By default, the pod template within the deployment defines the labels `app: {{ template "common.name" . }}` -and `release: {{ .Release.Name | quote }` as this is also used as the selector. The -standard set of labels are not used as some of these can change during upgrades, -which causes the replica sets and pods to not correctly match. - -Example use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -## Define overrides for your Deployment resource here, e.g. -spec: - replicas: {{ .Values.replicaCount }} -{{- end -}} -``` - -## `common.container` - -The `common.container` template creates a basic `Container` spec to be used -within a `Deployment` or `ReplicaSet`. It holds the following defaults: - -- The name is set to the chart name -- Uses `.Values.image` to describe the image to run, with the following spec: - ```yaml - image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent - ``` -- Exposes the named port `http` as port 80 -- Lays out the compute resources using `.Values.resources` - -Example use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -## Define overrides for your Deployment resource here, e.g. -spec: - template: - spec: - containers: - - {{ template "common.container" (list . "mychart.deployment.container") }} -{{- end -}} -{{- define "mychart.deployment.container" -}} -## Define overrides for your Container here, e.g. -livenessProbe: - httpGet: - path: / - port: 80 -readinessProbe: - httpGet: - path: / - port: 80 -{{- end -}} -``` - -The above example creates a `Deployment` resource which makes use of the -`common.container` template to populate the PodSpec's container list. The usage -of this template is similar to the other resources, you must define and -reference a template that contains overrides for the container object. - -The most important part of a container definition is the image you want to run. -As mentioned above, this is derived from `.Values.image` by default. It is a -best practice to define the image, tag and pull policy in your charts' values as -this makes it easy for an operator to change the image registry, or use a -specific tag or version. Another example of configuration that should be exposed -to chart operators is the container's required compute resources, as this is -also very specific to an operators environment. An example `values.yaml` for -your chart could look like: - -```yaml -image: - repository: nginx - tag: stable - pullPolicy: IfNotPresent -resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi -``` - -The output of running the above values through the earlier template is: - -```yaml -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - labels: - app.kubernetes.io/name: deployment - helm.sh/chart: deployment-0.1.0 - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/instance: release-name - name: release-name-deployment -spec: - template: - metadata: - labels: - app.kubernetes.io/name: deployment - spec: - containers: - - image: nginx:stable - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: 80 - name: deployment - ports: - - containerPort: 80 - name: http - readinessProbe: - httpGet: - path: / - port: 80 - resources: - limits: - cpu: 100m - memory: 128Mi - requests: - cpu: 100m - memory: 128Mi -``` - -## `common.configmap` - -The `common.configmap` template creates an empty `ConfigMap` resource that you -can override with your configuration. - -Example use: - -```yaml -{{- template "common.configmap" (list . "mychart.configmap") -}} -{{- define "mychart.configmap" -}} -data: - zeus: cat - athena: cat - julius: cat - one: |- - {{ .Files.Get "file1.txt" }} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: v1 -data: - athena: cat - julius: cat - one: This is a file. - zeus: cat -kind: ConfigMap -metadata: - labels: - app.kubernetes.io/name: configmap - helm.sh/chart: configmap-0.1.0 - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/instance: release-name - name: release-name-configmap -``` - -## `common.secret` - -The `common.secret` template creates an empty `Secret` resource that you -can override with your secrets. - -Example use: - -```yaml -{{- template "common.secret" (list . "mychart.secret") -}} -{{- define "mychart.secret" -}} -data: - zeus: {{ print "cat" | b64enc }} - athena: {{ print "cat" | b64enc }} - julius: {{ print "cat" | b64enc }} - one: |- - {{ .Files.Get "file1.txt" | b64enc }} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: v1 -data: - athena: Y2F0 - julius: Y2F0 - one: VGhpcyBpcyBhIGZpbGUuCg== - zeus: Y2F0 -kind: Secret -metadata: - labels: - app.kubernetes.io/name: secret - helm.sh/chart: secret-0.1.0 - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/instance: release-name - name: release-name-secret -type: Opaque -``` - -## `common.ingress` - -The `common.ingress` template is designed to give you a well-defined `Ingress` -resource, that can be configured using `.Values.ingress`. An example values file -that can be used to configure the `Ingress` resource is: - -```yaml -ingress: - hosts: - - chart-example.local - annotations: - kubernetes.io/ingress.class: nginx - kubernetes.io/tls-acme: "true" - tls: - - secretName: chart-example-tls - hosts: - - chart-example.local -``` - -Example use: - -```yaml -{{- template "common.ingress" (list . "mychart.ingress") -}} -{{- define "mychart.ingress" -}} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: extensions/v1beta1 -kind: Ingress -metadata: - annotations: - kubernetes.io/ingress.class: nginx - kubernetes.io/tls-acme: "true" - labels: - app.kubernetes.io/name: ingress - helm.sh/chart: ingress-0.1.0 - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/instance: release-name - name: release-name-ingress -spec: - rules: - - host: chart-example.local - http: - paths: - - backend: - serviceName: release-name-ingress - servicePort: 80 - path: / - tls: - - hosts: - - chart-example.local - secretName: chart-example-tls -``` - -## `common.persistentvolumeclaim` - -`common.persistentvolumeclaim` can be used to easily add a -`PersistentVolumeClaim` resource to your chart that can be configured using -`.Values.persistence`: - -| Value | Description | -| ------------------------- | ------------------------------------------------------------------------------------------------------- | -| persistence.enabled | Whether or not to claim a persistent volume. If false, `common.volume.pvc` will use an emptyDir instead | -| persistence.storageClass | `StorageClass` name | -| persistence.accessMode | Access mode for persistent volume | -| persistence.size | Size of persistent volume | -| persistence.existingClaim | If defined, `PersistentVolumeClaim` is not created and `common.volume.pvc` helper uses this claim | - -An example values file that can be used to configure the -`PersistentVolumeClaim` resource is: - -```yaml -persistence: - enabled: true - storageClass: fast - accessMode: ReadWriteOnce - size: 8Gi -``` - -Example use: - -```yaml -{{- template "common.persistentvolumeclaim" (list . "mychart.persistentvolumeclaim") -}} -{{- define "mychart.persistentvolumeclaim" -}} -{{- end -}} -``` - -Output: - -```yaml -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - labels: - app.kubernetes.io/name: persistentvolumeclaim - helm.sh/chart: persistentvolumeclaim-0.1.0 - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/instance: release-name - name: release-name-persistentvolumeclaim -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 8Gi - storageClassName: "fast" -``` - -## Partial API Objects - -When writing Kubernetes resources, you may find the following helpers useful to -construct parts of the spec. - -### EnvVar - -Use the EnvVar helpers within a container spec to simplify specifying key-value -environment variables or referencing secrets as values. - -Example Use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -spec: - template: - spec: - containers: - - {{ template "common.container" (list . "mychart.deployment.container") }} -{{- end -}} -{{- define "mychart.deployment.container" -}} -{{- $fullname := include "common.fullname" . -}} -env: -- {{ template "common.envvar.value" (list "ZEUS" "cat") }} -- {{ template "common.envvar.secret" (list "ATHENA" "secret-name" "athena") }} -{{- end -}} -``` - -Output: - -```yaml -... - spec: - containers: - - env: - - name: ZEUS - value: cat - - name: ATHENA - valueFrom: - secretKeyRef: - key: athena - name: secret-name -... -``` - -### Volume - -Use the Volume helpers within a `Deployment` spec to help define ConfigMap and -PersistentVolumeClaim volumes. - -Example Use: - -```yaml -{{- template "common.deployment" (list . "mychart.deployment") -}} -{{- define "mychart.deployment" -}} -spec: - template: - spec: - volumes: - - {{ template "common.volume.configMap" (list "config" "configmap-name") }} - - {{ template "common.volume.pvc" (list "data" "pvc-name" .Values.persistence) }} -{{- end -}} -``` - -Output: - -```yaml -... - spec: - volumes: - - configMap: - name: configmap-name - name: config - - name: data - persistentVolumeClaim: - claimName: pvc-name -... -``` - -The `common.volume.pvc` helper uses the following configuration from the `.Values.persistence` object: - -| Value | Description | -| ------------------------- | ----------------------------------------------------- | -| persistence.enabled | If false, creates an `emptyDir` instead | -| persistence.existingClaim | If set, uses this instead of the passed in claim name | - -## Utilities - -### `common.fullname` - -The `common.fullname` template generates a name suitable for the `name:` field -in Kubernetes metadata. It is used like this: - -```yaml -name: {{ template "common.fullname" . }} -``` - -The following different values can influence it: - -```yaml -# By default, fullname uses '{{ .Release.Name }}-{{ .Chart.Name }}'. This -# overrides that and uses the given string instead. -fullnameOverride: "some-name" - -# This adds a prefix -fullnamePrefix: "pre-" -# This appends a suffix -fullnameSuffix: "-suf" - -# Global versions of the above -global: - fullnamePrefix: "pp-" - fullnameSuffix: "-ps" -``` - -Example output: - -```yaml ---- -# with the values above -name: pp-pre-some-name-suf-ps - ---- -# the default, for release "happy-panda" and chart "wordpress" -name: happy-panda-wordpress -``` - -Output of this function is truncated at 54 characters, which leaves 9 additional -characters for customized overriding. Thus you can easily extend this name -in your own charts: - -```yaml -{{- define "my.fullname" -}} - {{ template "common.fullname" . }}-my-stuff -{{- end -}} -``` - -### `common.fullname.unique` - -The `common.fullname.unique` variant of fullname appends a unique seven-character -sequence to the end of the common name field. - -This takes all of the same parameters as `common.fullname` - -Example template: - -```yaml -uniqueName: {{ template "common.fullname.unique" . }} -``` - -Example output: - -```yaml -uniqueName: release-name-fullname-jl0dbwx -``` - -It is also impacted by the prefix and suffix definitions, as well as by -`.Values.fullnameOverride` - -Note that the effective maximum length of this function is 63 characters, not 54. - -### `common.name` - -The `common.name` template generates a name suitable for the `app` label. It is used like this: - -```yaml -app: {{ template "common.name" . }} -``` - -The following different values can influence it: - -```yaml -# By default, name uses '{{ .Chart.Name }}'. This -# overrides that and uses the given string instead. -nameOverride: "some-name" - -# This adds a prefix -namePrefix: "pre-" -# This appends a suffix -nameSuffix: "-suf" - -# Global versions of the above -global: - namePrefix: "pp-" - nameSuffix: "-ps" -``` - -Example output: - -```yaml ---- -# with the values above -name: pp-pre-some-name-suf-ps - ---- -# the default, for chart "wordpress" -name: wordpress -``` - -Output of this function is truncated at 54 characters, which leaves 9 additional -characters for customized overriding. Thus you can easily extend this name -in your own charts: - -```yaml -{{- define "my.name" -}} - {{ template "common.name" . }}-my-stuff -{{- end -}} -``` - -### `common.metadata` - -The `common.metadata` helper generates the `metadata:` section of a Kubernetes -resource. - -This takes three objects: - - .top: top context - - .fullnameOverride: override the fullname with this name - - .metadata - - .labels: key/value list of labels - - .annotations: key/value list of annotations - - .hook: name(s) of hook(s) - -It generates standard labels, annotations, hooks, and a name field. - -Example template: - -```yaml -{{ template "common.metadata" (dict "top" . "metadata" .Values.bio) }} ---- -{{ template "common.metadata" (dict "top" . "metadata" .Values.pet "fullnameOverride" .Values.pet.fullnameOverride) }} -``` - -Example values: - -```yaml -bio: - name: example - labels: - first: matt - last: butcher - nick: technosophos - annotations: - format: bio - destination: archive - hook: pre-install - -pet: - fullnameOverride: Zeus - -``` - -Example output: - -```yaml -metadata: - name: release-name-metadata - labels: - app.kubernetes.io/name: metadata - app.kubernetes.io/managed-by: "Helm" - app.kubernetes.io/instance: "release-name" - helm.sh/chart: metadata-0.1.0 - first: "matt" - last: "butcher" - nick: "technosophos" - annotations: - "destination": "archive" - "format": "bio" - "helm.sh/hook": "pre-install" ---- -metadata: - name: Zeus - labels: - app.kubernetes.io/name: metadata - app.kubernetes.io/managed-by: "Helm" - app.kubernetes.io/instance: "release-name" - helm.sh/chart: metadata-0.1.0 - annotations: -``` - -Most of the common templates that define a resource type (e.g. `common.configmap` -or `common.job`) use this to generate the metadata, which means they inherit -the same `labels`, `annotations`, `nameOverride`, and `hook` fields. - -### `common.labelize` - -`common.labelize` turns a map into a set of labels. - -Example template: - -```yaml -{{- $map := dict "first" "1" "second" "2" "third" "3" -}} -{{- template "common.labelize" $map -}} -``` - -Example output: - -```yaml -first: "1" -second: "2" -third: "3" -``` - -### `common.labels.standard` - -`common.labels.standard` prints the standard set of labels. - -Example usage: - -``` -{{ template "common.labels.standard" . }} -``` - -Example output: - -```yaml -app.kubernetes.io/name: labelizer -app.kubernetes.io/managed-by: "Tiller" -app.kubernetes.io/instance: "release-name" -helm.sh/chart: labelizer-0.1.0 -``` - -### `common.hook` - -The `common.hook` template is a convenience for defining hooks. - -Example template: - -```yaml -{{ template "common.hook" "pre-install,post-install" }} -``` - -Example output: - -```yaml -"helm.sh/hook": "pre-install,post-install" -``` - -### `common.chartref` - -The `common.chartref` helper prints the chart name and version, escaped to be -legal in a Kubernetes label field. - -Example template: - -```yaml -chartref: {{ template "common.chartref" . }} -``` - -For the chart `foo` with version `1.2.3-beta.55+1234`, this will render: - -```yaml -chartref: foo-1.2.3-beta.55_1234 -``` - -(Note that `+` is an illegal character in label values) diff --git a/pkg/helm/cmd/helm/testdata/testplugin/plugin.yaml b/pkg/helm/cmd/helm/testdata/testplugin/plugin.yaml deleted file mode 100644 index 890292cb..00000000 --- a/pkg/helm/cmd/helm/testdata/testplugin/plugin.yaml +++ /dev/null @@ -1,4 +0,0 @@ -name: testplugin -usage: "echo test" -description: "This echos test" -command: "echo test" diff --git a/pkg/helm/cmd/helm/uninstall.go b/pkg/helm/cmd/helm/uninstall.go deleted file mode 100644 index e98af025..00000000 --- a/pkg/helm/cmd/helm/uninstall.go +++ /dev/null @@ -1,117 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "io" - "time" - - "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" - "github.com/werf/nelm/pkg/helm/pkg/action" - "github.com/werf/nelm/pkg/helm/pkg/phases" -) - -const uninstallDesc = ` -This command takes a release name and uninstalls the release. - -It removes all of the resources associated with the last release of the chart -as well as the release history, freeing it up for future use. - -Use the '--dry-run' flag to see which releases will be uninstalled without actually -uninstalling them. -` - -func NewUninstallCmd(cfg *action.Configuration, out io.Writer, opts UninstallCmdOptions) *cobra.Command { - client := action.NewUninstall(cfg, opts.StagesSplitter) - - cmd := &cobra.Command{ - Use: "uninstall RELEASE_NAME [...]", - Aliases: []string{"del", "delete", "un"}, - SuggestFor: []string{"remove", "rm"}, - Short: "uninstall a release", - Long: uninstallDesc, - Args: require.MinimumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return compListReleases(toComplete, args, cfg) - }, - RunE: func(cmd *cobra.Command, args []string) error { - validationErr := validateCascadeFlag(client) - if validationErr != nil { - return validationErr - } - - if opts.DeleteHooks != nil { - client.DeleteHooks = *opts.DeleteHooks - } - if opts.DeleteNamespace != nil { - client.DeleteNamespace = *opts.DeleteNamespace - } - if opts.DontFailIfNoRelease != nil { - client.IgnoreNotFound = *opts.DontFailIfNoRelease - } - - client.Namespace = Settings.Namespace() - - for i := 0; i < len(args); i++ { - - res, err := client.Run(args[i]) - if err != nil { - return err - } - if res != nil && res.Info != "" { - fmt.Fprintln(out, res.Info) - } - - fmt.Fprintf(out, "release \"%s\" uninstalled\n", args[i]) - } - return nil - }, - } - - f := cmd.Flags() - f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall") - f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation") - f.BoolVar(&client.IgnoreNotFound, "ignore-not-found", false, `Treat "release not found" as a successful uninstall`) - f.BoolVar(&client.KeepHistory, "keep-history", false, "remove all associated resources and mark the release as deleted, but retain the release history") - f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all the resources are deleted before returning. It will wait for as long as --timeout") - f.StringVar(&client.DeletionPropagation, "cascade", "background", "Must be \"background\", \"orphan\", or \"foreground\". Selects the deletion cascading strategy for the dependents. Defaults to background.") - f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") - f.StringVar(&client.Description, "description", "", "add a custom description") - - return cmd -} - -func validateCascadeFlag(client *action.Uninstall) error { - if client.DeletionPropagation != "background" && client.DeletionPropagation != "foreground" && client.DeletionPropagation != "orphan" { - return fmt.Errorf("invalid cascade value (%s). Must be \"background\", \"foreground\", or \"orphan\"", client.DeletionPropagation) - } - return nil -} - -func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - return NewUninstallCmd(cfg, out, UninstallCmdOptions{}) -} - -type UninstallCmdOptions struct { - StagesSplitter phases.Splitter - DeleteNamespace *bool - DeleteHooks *bool - - DontFailIfNoRelease *bool -} diff --git a/pkg/helm/cmd/helm/verify_test.go b/pkg/helm/cmd/helm/verify_test.go deleted file mode 100644 index 4ffe546f..00000000 --- a/pkg/helm/cmd/helm/verify_test.go +++ /dev/null @@ -1,97 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package helm - -import ( - "fmt" - "runtime" - "testing" -) - -func TestVerifyCmd(t *testing.T) { - - statExe := "stat" - statPathMsg := "no such file or directory" - statFileMsg := statPathMsg - if runtime.GOOS == "windows" { - statExe = "FindFirstFile" - statPathMsg = "The system cannot find the path specified." - statFileMsg = "The system cannot find the file specified." - } - - tests := []struct { - name string - cmd string - expect string - wantError bool - }{ - { - name: "verify requires a chart", - cmd: "verify", - expect: "\"helm verify\" requires 1 argument\n\nUsage: helm verify PATH [flags]", - wantError: true, - }, - { - name: "verify requires that chart exists", - cmd: "verify no/such/file", - expect: fmt.Sprintf("%s no/such/file: %s", statExe, statPathMsg), - wantError: true, - }, - { - name: "verify requires that chart is not a directory", - cmd: "verify testdata/testcharts/signtest", - expect: "unpacked charts cannot be verified", - wantError: true, - }, - { - name: "verify requires that chart has prov file", - cmd: "verify testdata/testcharts/compressedchart-0.1.0.tgz", - expect: fmt.Sprintf("could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s", statExe, statFileMsg), - wantError: true, - }, - { - name: "verify validates a properly signed chart", - cmd: "verify testdata/testcharts/signtest-0.1.0.tgz --keyring testdata/helm-test-key.pub", - expect: "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \nUsing Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\nChart Hash Verified: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\n", - wantError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, out, err := executeActionCommand(tt.cmd) - if tt.wantError { - if err == nil { - t.Errorf("Expected error, but got none: %q", out) - } - if err.Error() != tt.expect { - t.Errorf("Expected error %q, got %q", tt.expect, err) - } - return - } else if err != nil { - t.Errorf("Unexpected error: %s", err) - } - if out != tt.expect { - t.Errorf("Expected %q, got %q", tt.expect, out) - } - }) - } -} - -func TestVerifyFileCompletion(t *testing.T) { - checkFileCompletion(t, "verify", true) - checkFileCompletion(t, "verify mypath", false) -} diff --git a/pkg/helm/intern/chart/v3/chart.go b/pkg/helm/intern/chart/v3/chart.go new file mode 100644 index 00000000..577a7843 --- /dev/null +++ b/pkg/helm/intern/chart/v3/chart.go @@ -0,0 +1,183 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +// APIVersionV3 is the API version number for version 3. +const APIVersionV3 = "v3" + +// aliasNameFormat defines the characters that are legal in an alias name. +var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$") + +// Chart is a helm package that contains metadata, a default config, zero or more +// optionally parameterizable templates, and zero or more charts (dependencies). +type Chart struct { + // Raw contains the raw contents of the files originally contained in the chart archive. + // + // This should not be used except in special cases like `helm show values`, + // where we want to display the raw values, comments and all. + Raw []*common.File `json:"-"` + // Metadata is the contents of the Chartfile. + Metadata *Metadata `json:"metadata"` + // Lock is the contents of Chart.lock. + Lock *Lock `json:"lock"` + // Templates for this chart. + Templates []*common.File `json:"templates"` + // Values are default config for this chart. + Values map[string]interface{} `json:"values"` + // Schema is an optional JSON schema for imposing structure on Values + Schema []byte `json:"schema"` + // SchemaModTime the schema was last modified + SchemaModTime time.Time `json:"schemamodtime,omitempty"` + // Files are miscellaneous files in a chart archive, + // e.g. README, LICENSE, etc. + Files []*common.File `json:"files"` + // ModTime the chart metadata was last modified + ModTime time.Time `json:"modtime,omitzero"` + + RuntimeFiles []*common.File `json:"-"` + ExtraValues map[string]interface{} `json:"-"` + SecretsRuntimeData common.RuntimeData `json:"-"` + + parent *Chart + dependencies []*Chart +} + +type CRD struct { + // Name is the File.Name for the crd file + Name string + // Filename is the File obj Name including (sub-)chart.ChartFullPath + Filename string + // File is the File obj for the crd + File *common.File +} + +// SetDependencies replaces the chart dependencies. +func (ch *Chart) SetDependencies(charts ...*Chart) { + ch.dependencies = nil + ch.AddDependency(charts...) +} + +// Name returns the name of the chart. +func (ch *Chart) Name() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.Name +} + +// AddDependency determines if the chart is a subchart. +func (ch *Chart) AddDependency(charts ...*Chart) { + for i, x := range charts { + charts[i].parent = ch + ch.dependencies = append(ch.dependencies, x) + } +} + +// Root finds the root chart. +func (ch *Chart) Root() *Chart { + if ch.IsRoot() { + return ch + } + return ch.Parent().Root() +} + +// Dependencies are the charts that this chart depends on. +func (ch *Chart) Dependencies() []*Chart { return ch.dependencies } + +// IsRoot determines if the chart is the root chart. +func (ch *Chart) IsRoot() bool { return ch.parent == nil } + +// Parent returns a subchart's parent chart. +func (ch *Chart) Parent() *Chart { return ch.parent } + +// ChartPath returns the full path to this chart in dot notation. +func (ch *Chart) ChartPath() string { + if !ch.IsRoot() { + return ch.Parent().ChartPath() + "." + ch.Name() + } + return ch.Name() +} + +// ChartFullPath returns the full path to this chart. +// Note that the path may not correspond to the path where the file can be found on the file system if the path +// points to an aliased subchart. +func (ch *Chart) ChartFullPath() string { + if !ch.IsRoot() { + return ch.Parent().ChartFullPath() + "/charts/" + ch.Name() + } + return ch.Name() +} + +// Validate validates the metadata. +func (ch *Chart) Validate() error { + return ch.Metadata.Validate() +} + +// AppVersion returns the appversion of the chart. +func (ch *Chart) AppVersion() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.AppVersion +} + +// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. +// Deprecated: use CRDObjects() +func (ch *Chart) CRDs() []*common.File { + files := []*common.File{} + // Find all resources in the crds/ directory + for _, f := range ch.Files { + if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { + files = append(files, f) + } + } + // Get CRDs from dependencies, too. + for _, dep := range ch.Dependencies() { + files = append(files, dep.CRDs()...) + } + return files +} + +// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts +func (ch *Chart) CRDObjects() []CRD { + crds := []CRD{} + // Find all resources in the crds/ directory + for _, f := range ch.Files { + if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { + mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f} + crds = append(crds, mycrd) + } + } + // Get CRDs from dependencies, too. + for _, dep := range ch.Dependencies() { + crds = append(crds, dep.CRDObjects()...) + } + return crds +} + +func hasManifestExtension(fname string) bool { + ext := filepath.Ext(fname) + return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json") +} diff --git a/pkg/helm/intern/chart/v3/chart_test.go b/pkg/helm/intern/chart/v3/chart_test.go new file mode 100644 index 00000000..979e4818 --- /dev/null +++ b/pkg/helm/intern/chart/v3/chart_test.go @@ -0,0 +1,229 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v3 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +func TestCRDs(t *testing.T) { + modTime := time.Now() + chrt := Chart{ + Files: []*common.File{ + { + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDs() + is.Equal(2, len(crds)) + is.Equal("crds/foo.yaml", crds[0].Name) + is.Equal("crds/foo/bar/baz.yaml", crds[1].Name) +} + +func TestSaveChartNoRawData(t *testing.T) { + chrt := Chart{ + Raw: []*common.File{ + { + Name: "fhqwhgads.yaml", + ModTime: time.Now(), + Data: []byte("Everybody to the Limit"), + }, + }, + } + + is := assert.New(t) + data, err := json.Marshal(chrt) + if err != nil { + t.Fatal(err) + } + + res := &Chart{} + if err := json.Unmarshal(data, res); err != nil { + t.Fatal(err) + } + + is.Equal([]*common.File(nil), res.Raw) +} + +func TestMetadata(t *testing.T) { + chrt := Chart{ + Metadata: &Metadata{ + Name: "foo.yaml", + AppVersion: "1.0.0", + APIVersion: "v3", + Version: "1.0.0", + Type: "application", + }, + } + + is := assert.New(t) + + is.Equal("foo.yaml", chrt.Name()) + is.Equal("1.0.0", chrt.AppVersion()) + is.Equal(nil, chrt.Validate()) +} + +func TestIsRoot(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal(false, chrt1.IsRoot()) + is.Equal(true, chrt2.IsRoot()) +} + +func TestChartPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo.", chrt1.ChartPath()) + is.Equal("foo", chrt2.ChartPath()) +} + +func TestChartFullPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo/charts/", chrt1.ChartFullPath()) + is.Equal("foo", chrt2.ChartFullPath()) +} + +func TestCRDObjects(t *testing.T) { + modTime := time.Now() + chrt := Chart{ + Files: []*common.File{ + { + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), + }, + }, + } + + expected := []CRD{ + { + Name: "crds/foo.yaml", + Filename: "crds/foo.yaml", + File: &common.File{ + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + }, + { + Name: "crds/foo/bar/baz.yaml", + Filename: "crds/foo/bar/baz.yaml", + File: &common.File{ + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDObjects() + is.Equal(expected, crds) +} diff --git a/pkg/helm/intern/chart/v3/dependency.go b/pkg/helm/intern/chart/v3/dependency.go new file mode 100644 index 00000000..2d956b54 --- /dev/null +++ b/pkg/helm/intern/chart/v3/dependency.go @@ -0,0 +1,82 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import "time" + +// Dependency describes a chart upon which another chart depends. +// +// Dependencies can be used to express developer intent, or to capture the state +// of a chart. +type Dependency struct { + // Name is the name of the dependency. + // + // This must mach the name in the dependency's Chart.yaml. + Name string `json:"name" yaml:"name"` + // Version is the version (range) of this chart. + // + // A lock file will always produce a single version, while a dependency + // may contain a semantic version range. + Version string `json:"version,omitempty" yaml:"version,omitempty"` + // The URL to the repository. + // + // Appending `index.yaml` to this string should result in a URL that can be + // used to fetch the repository index. + Repository string `json:"repository" yaml:"repository"` + // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) + Condition string `json:"condition,omitempty" yaml:"condition,omitempty"` + // Tags can be used to group charts for enabling/disabling together + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + // Enabled bool determines if chart should be loaded + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a + // string or pair of child/parent sublist items. + ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"` + // Alias usable alias to be used for the chart + Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` +} + +// Validate checks for common problems with the dependency datastructure in +// the chart. This check must be done at load time before the dependency's charts are +// loaded. +func (d *Dependency) Validate() error { + if d == nil { + return ValidationError("dependencies must not contain empty or null nodes") + } + d.Name = sanitizeString(d.Name) + d.Version = sanitizeString(d.Version) + d.Repository = sanitizeString(d.Repository) + d.Condition = sanitizeString(d.Condition) + for i := range d.Tags { + d.Tags[i] = sanitizeString(d.Tags[i]) + } + if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) { + return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name) + } + return nil +} + +// Lock is a lock file for dependencies. +// +// It represents the state that the dependencies should be in. +type Lock struct { + // Generated is the date the lock file was last generated. + Generated time.Time `json:"generated"` + // Digest is a hash of the dependencies in Chart.yaml. + Digest string `json:"digest"` + // Dependencies is the list of dependencies that this lock file has locked. + Dependencies []*Dependency `json:"dependencies"` +} diff --git a/pkg/helm/intern/chart/v3/dependency_test.go b/pkg/helm/intern/chart/v3/dependency_test.go new file mode 100644 index 00000000..fcea19ae --- /dev/null +++ b/pkg/helm/intern/chart/v3/dependency_test.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v3 + +import ( + "testing" +) + +func TestValidateDependency(t *testing.T) { + dep := &Dependency{ + Name: "example", + } + for value, shouldFail := range map[string]bool{ + "abcdefghijklmenopQRSTUVWXYZ-0123456780_": false, + "-okay": false, + "_okay": false, + "- bad": true, + " bad": true, + "bad\nvalue": true, + "bad ": true, + "bad$": true, + } { + dep.Alias = value + res := dep.Validate() + if res != nil && !shouldFail { + t.Errorf("Failed on case %q", dep.Alias) + } else if res == nil && shouldFail { + t.Errorf("Expected failure for %q", dep.Alias) + } + } +} diff --git a/pkg/helm/intern/chart/v3/doc.go b/pkg/helm/intern/chart/v3/doc.go new file mode 100644 index 00000000..e003833a --- /dev/null +++ b/pkg/helm/intern/chart/v3/doc.go @@ -0,0 +1,21 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package v3 provides chart handling for apiVersion v3 charts + +This package and its sub-packages provide handling for apiVersion v3 charts. +*/ +package v3 diff --git a/pkg/helm/intern/chart/v3/errors.go b/pkg/helm/intern/chart/v3/errors.go new file mode 100644 index 00000000..059e43f0 --- /dev/null +++ b/pkg/helm/intern/chart/v3/errors.go @@ -0,0 +1,30 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import "fmt" + +// ValidationError represents a data validation error. +type ValidationError string + +func (v ValidationError) Error() string { + return "validation: " + string(v) +} + +// ValidationErrorf takes a message and formatting options and creates a ValidationError +func ValidationErrorf(msg string, args ...interface{}) ValidationError { + return ValidationError(fmt.Sprintf(msg, args...)) +} diff --git a/pkg/helm/intern/chart/v3/fuzz_test.go b/pkg/helm/intern/chart/v3/fuzz_test.go new file mode 100644 index 00000000..982c2648 --- /dev/null +++ b/pkg/helm/intern/chart/v3/fuzz_test.go @@ -0,0 +1,48 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "testing" + + fuzz "github.com/AdaLogics/go-fuzz-headers" +) + +func FuzzMetadataValidate(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + fdp := fuzz.NewConsumer(data) + // Add random values to the metadata + md := &Metadata{} + err := fdp.GenerateStruct(md) + if err != nil { + t.Skip() + } + md.Validate() + }) +} + +func FuzzDependencyValidate(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + f := fuzz.NewConsumer(data) + // Add random values to the dependenci + d := &Dependency{} + err := f.GenerateStruct(d) + if err != nil { + t.Skip() + } + d.Validate() + }) +} diff --git a/pkg/helm/intern/chart/v3/lint/lint.go b/pkg/helm/intern/chart/v3/lint/lint.go new file mode 100644 index 00000000..85b46322 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/lint.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint // import "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint" + +import ( + "path/filepath" + + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/rules" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +type linterOptions struct { + KubeVersion *common.KubeVersion + SkipSchemaValidation bool +} + +type LinterOption func(lo *linterOptions) + +func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption { + return func(lo *linterOptions) { + lo.KubeVersion = kubeVersion + } +} + +func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption { + return func(lo *linterOptions) { + lo.SkipSchemaValidation = skipSchemaValidation + } +} + +func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter { + + chartDir, _ := filepath.Abs(baseDir) + + lo := linterOptions{} + for _, option := range options { + option(&lo) + } + + result := support.Linter{ + ChartDir: chartDir, + } + + rules.Chartfile(&result) + rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation) + rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) + rules.Dependencies(&result) + rules.Crds(&result) + + return result +} diff --git a/pkg/helm/intern/chart/v3/lint/lint_test.go b/pkg/helm/intern/chart/v3/lint/lint_test.go new file mode 100644 index 00000000..1b776600 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/lint_test.go @@ -0,0 +1,243 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" +) + +const namespace = "testNamespace" + +const badChartDir = "rules/testdata/badchartfile" +const badValuesFileDir = "rules/testdata/badvaluesfile" +const badYamlFileDir = "rules/testdata/albatross" +const badCrdFileDir = "rules/testdata/badcrdfile" +const goodChartDir = "rules/testdata/goodone" +const subChartValuesDir = "rules/testdata/withsubchart" +const malformedTemplate = "rules/testdata/malformed-template" +const invalidChartFileDir = "rules/testdata/invalidchartfile" + +func TestBadChartV3(t *testing.T) { + var values map[string]any + m := RunAll(badChartDir, values, namespace).Messages + if len(m) != 8 { + t.Errorf("Number of errors %v", len(m)) + t.Errorf("All didn't fail with expected errors, got %#v", m) + } + // There should be one INFO, one WARNING, and 2 ERROR messages, check for them + var i, w, e, e2, e3, e4, e5, e6 bool + for _, msg := range m { + if msg.Severity == support.InfoSev { + if strings.Contains(msg.Err.Error(), "icon is recommended") { + i = true + } + } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "does not exist") { + w = true + } + } + if msg.Severity == support.ErrorSev { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") { + e = true + } + if strings.Contains(msg.Err.Error(), "name is required") { + e2 = true + } + + if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be \"v3\"") { + e3 = true + } + + if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { + e4 = true + } + + if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + e5 = true + } + // This comes from the dependency check, which loads dependency info from the Chart.yaml + if strings.Contains(msg.Err.Error(), "unable to load chart") { + e6 = true + } + } + } + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w { + t.Errorf("Didn't find all the expected errors, got %#v", m) + } +} + +func TestInvalidYaml(t *testing.T) { + var values map[string]any + m := RunAll(badYamlFileDir, values, namespace).Messages + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("All didn't have the error for deliberateSyntaxError") + } +} + +func TestInvalidChartYamlV3(t *testing.T) { + var values map[string]any + m := RunAll(invalidChartFileDir, values, namespace).Messages + t.Log(m) + if len(m) != 3 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") { + t.Errorf("All didn't have the error for duplicate YAML keys") + } +} + +func TestBadValuesV3(t *testing.T) { + var values map[string]any + m := RunAll(badValuesFileDir, values, namespace).Messages + if len(m) < 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { + t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) + } +} + +func TestBadCrdFileV3(t *testing.T) { + var values map[string]any + m := RunAll(badCrdFileDir, values, namespace).Messages + assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) + assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") + assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'") +} + +func TestGoodChart(t *testing.T) { + var values map[string]any + m := RunAll(goodChartDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. +// +// See https://github.com/helm/helm/issues/7923 +func TestHelmCreateChart(t *testing.T) { + var values map[string]any + dir := t.TempDir() + + createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) + if err != nil { + t.Error(err) + // Fatal is bad because of the defer. + return + } + + // Note: we test with strict=true here, even though others have + // strict = false. + m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages + if ll := len(m); ll != 1 { + t.Errorf("All should have had exactly 1 error. Got %d", ll) + for i, msg := range m { + t.Logf("Message %d: %s", i, msg.Error()) + } + } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { + t.Errorf("Unexpected lint error: %s", msg) + } +} + +// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws +// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags). +// +// See https://github.com/helm/helm/issues/11495 +// +// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent +// of the `--set` flag. +func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { + createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir()) + if err != nil { + t.Error(err) + return + } + + // Add values to enable hpa, and ingress which are disabled by default. + // This is the equivalent of: + // helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true' + updatedValues := map[string]any{ + "autoscaling": map[string]any{ + "enabled": true, + }, + "ingress": map[string]any{ + "enabled": true, + }, + } + + linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true)) + for _, msg := range linterRunDetails.Messages { + if strings.HasPrefix(msg.Error(), "[WARNING]") && + strings.Contains(msg.Error(), "deprecated") { + // When there is a deprecation warning for an object created + // by `helm create` for the current Kubernetes version, fail. + t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error()) + } + } +} + +// lint ignores import-values +// See https://github.com/helm/helm/issues/9658 +func TestSubChartValuesChart(t *testing.T) { + var values map[string]any + m := RunAll(subChartValuesDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// lint stuck with malformed template object +// See https://github.com/helm/helm/issues/11391 +func TestMalformedTemplate(t *testing.T) { + var values map[string]any + c := time.After(3 * time.Second) + ch := make(chan int, 1) + var m []support.Message + go func() { + m = RunAll(malformedTemplate, values, namespace).Messages + ch <- 1 + }() + select { + case <-c: + t.Fatalf("lint malformed template timeout") + case <-ch: + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { + t.Errorf("All didn't have the error for invalid character '{'") + } + } +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/chartfile.go b/pkg/helm/intern/chart/v3/lint/rules/chartfile.go new file mode 100644 index 00000000..aab053eb --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/chartfile.go @@ -0,0 +1,225 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/rules" + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/asaskevich/govalidator" + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" +) + +// Chartfile runs a set of linter rules related to Chart.yaml file +func Chartfile(linter *support.Linter) { + chartFileName := "Chart.yaml" + chartPath := filepath.Join(linter.ChartDir, chartFileName) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) + + chartFile, err := chartutil.LoadChartfile(chartPath) + validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) + + // Guard clause. Following linter rules require a parsable ChartFile + if !validChartFile { + return + } + + _, err = chartutil.StrictLoadChartfile(chartPath) + linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err)) + + // type check for Chart.yaml . ignoring error as any parse + // errors would already be caught in the above load function + chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) + + // Chart metadata + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) + linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) +} + +func validateChartVersionType(data map[string]interface{}) error { + return isStringValue(data, "version") +} + +func validateChartAppVersionType(data map[string]interface{}) error { + return isStringValue(data, "appVersion") +} + +func isStringValue(data map[string]interface{}, key string) error { + value, ok := data[key] + if !ok { + return nil + } + valueType := fmt.Sprintf("%T", value) + if valueType != "string" { + return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType) + } + return nil +} + +func validateChartYamlNotDirectory(chartPath string) error { + fi, err := os.Stat(chartPath) + + if err == nil && fi.IsDir() { + return errors.New("should be a file, not a directory") + } + return nil +} + +func validateChartYamlFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError) + } + return nil +} + +func validateChartYamlStrictFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError) + } + return nil +} + +func validateChartName(cf *chart.Metadata) error { + if cf.Name == "" { + return errors.New("name is required") + } + name := filepath.Base(cf.Name) + if name != cf.Name { + return fmt.Errorf("chart name %q is invalid", cf.Name) + } + return nil +} + +func validateChartAPIVersion(cf *chart.Metadata) error { + if cf.APIVersion == "" { + return errors.New("apiVersion is required. The value must be \"v3\"") + } + + if cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("apiVersion '%s' is not valid. The value must be \"v3\"", cf.APIVersion) + } + + return nil +} + +func validateChartVersion(cf *chart.Metadata) error { + if cf.Version == "" { + return errors.New("version is required") + } + + version, err := semver.StrictNewVersion(cf.Version) + if err != nil { + return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version) + } + + c, err := semver.NewConstraint(">0.0.0-0") + if err != nil { + return err + } + valid, msg := c.Validate(version) + + if !valid && len(msg) > 0 { + return fmt.Errorf("version %v", msg[0]) + } + + return nil +} + +func validateChartMaintainer(cf *chart.Metadata) error { + for _, maintainer := range cf.Maintainers { + if maintainer == nil { + return errors.New("a maintainer entry is empty") + } + if maintainer.Name == "" { + return errors.New("each maintainer requires a name") + } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { + return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) + } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { + return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) + } + } + return nil +} + +func validateChartSources(cf *chart.Metadata) error { + for _, source := range cf.Sources { + if source == "" || !govalidator.IsRequestURL(source) { + return fmt.Errorf("invalid source URL '%s'", source) + } + } + return nil +} + +func validateChartIconPresence(cf *chart.Metadata) error { + if cf.Icon == "" { + return errors.New("icon is recommended") + } + return nil +} + +func validateChartIconURL(cf *chart.Metadata) error { + if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { + return fmt.Errorf("invalid icon URL '%s'", cf.Icon) + } + return nil +} + +func validateChartDependencies(cf *chart.Metadata) error { + if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3) + } + return nil +} + +func validateChartType(cf *chart.Metadata) error { + if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV3 { + return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV3) + } + return nil +} + +// loadChartFileForTypeCheck loads the Chart.yaml +// in a generic form of a map[string]interface{}, so that the type +// of the values can be checked +func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := make(map[string]interface{}) + err = yaml.Unmarshal(b, &y) + return y, err +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/chartfile_test.go b/pkg/helm/intern/chart/v3/lint/rules/chartfile_test.go new file mode 100644 index 00000000..b8b1b859 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/chartfile_test.go @@ -0,0 +1,278 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" +) + +const ( + badChartNameDir = "testdata/badchartname" + badChartDir = "testdata/badchartfile" + anotherBadChartDir = "testdata/anotherbadchartfile" +) + +var ( + badChartNamePath = filepath.Join(badChartNameDir, "Chart.yaml") + badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") + nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") +) + +var badChart, _ = chartutil.LoadChartfile(badChartFilePath) +var badChartName, _ = chartutil.LoadChartfile(badChartNamePath) + +// Validation functions Test +func TestValidateChartYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) + defer os.Remove(nonExistingChartFilePath) + + err := validateChartYamlNotDirectory(nonExistingChartFilePath) + if err == nil { + t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error") + } +} + +func TestValidateChartYamlFormat(t *testing.T) { + err := validateChartYamlFormat(errors.New("Read error")) + if err == nil { + t.Errorf("validateChartYamlFormat to return a linter error, got no error") + } + + err = validateChartYamlFormat(nil) + if err != nil { + t.Errorf("validateChartYamlFormat to return no error, got a linter error") + } +} + +func TestValidateChartName(t *testing.T) { + err := validateChartName(badChart) + if err == nil { + t.Errorf("validateChartName to return a linter error, got no error") + } + + err = validateChartName(badChartName) + if err == nil { + t.Error("expected validateChartName to return a linter error for an invalid name, got no error") + } +} + +func TestValidateChartVersion(t *testing.T) { + var failTest = []struct { + Version string + ErrorMsg string + }{ + {"", "version is required"}, + {"1.2.3.4", "version '1.2.3.4' is not a valid SemVerV2"}, + {"waps", "'waps' is not a valid SemVerV2"}, + {"-3", "'-3' is not a valid SemVerV2"}, + {"1.1", "'1.1' is not a valid SemVerV2"}, + {"1", "'1' is not a valid SemVerV2"}, + } + + var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} + + for _, test := range failTest { + badChart.Version = test.Version + err := validateChartVersion(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + } + } + + for _, version := range successTest { + badChart.Version = version + err := validateChartVersion(badChart) + if err != nil { + t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) + } + } +} + +func TestValidateChartMaintainer(t *testing.T) { + var failTest = []struct { + Name string + Email string + ErrorMsg string + }{ + {"", "", "each maintainer requires a name"}, + {"", "test@test.com", "each maintainer requires a name"}, + {"John Snow", "wrongFormatEmail.com", "invalid email"}, + } + + var successTest = []struct { + Name string + Email string + }{ + {"John Snow", ""}, + {"John Snow", "john@winterfell.com"}, + } + + for _, test := range failTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg) + } + } + + for _, test := range successTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err != nil { + t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error()) + } + } + + // Testing for an empty maintainer + badChart.Maintainers = []*chart.Maintainer{nil} + err := validateChartMaintainer(badChart) + if err == nil { + t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected") + } + if err.Error() != "a maintainer entry is empty" { + t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error()) + } +} + +func TestValidateChartSources(t *testing.T) { + var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"} + for _, test := range failTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid source URL") { + t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestValidateChartIconPresence(t *testing.T) { + t.Run("Icon absent", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "", + } + + err := validateChartIconPresence(testChart) + + if err == nil { + t.Errorf("validateChartIconPresence to return a linter error, got no error") + } else if !strings.Contains(err.Error(), "icon is recommended") { + t.Errorf("expected %q, got %q", "icon is recommended", err.Error()) + } + }) + t.Run("Icon present", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "http://example.org/icon.png", + } + + err := validateChartIconPresence(testChart) + + if err != nil { + t.Errorf("Unexpected error: %q", err.Error()) + } + }) +} + +func TestValidateChartIconURL(t *testing.T) { + var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"} + for _, test := range failTest { + badChart.Icon = test + err := validateChartIconURL(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid icon URL") { + t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Icon = test + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestV3Chartfile(t *testing.T) { + t.Run("Chart.yaml basic validity issues", func(t *testing.T) { + linter := support.Linter{ChartDir: badChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 6 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "name is required") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be \"v3\"") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + + if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } + }) + + t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { + linter := support.Linter{ChartDir: anotherBadChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 3 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + }) +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/crds.go b/pkg/helm/intern/chart/v3/lint/rules/crds.go new file mode 100644 index 00000000..8313a771 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/crds.go @@ -0,0 +1,116 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" +) + +// Crds lints the CRDs in the Linter. +func Crds(linter *support.Linter) { + fpath := "crds/" + crdsPath := filepath.Join(linter.ChartDir, fpath) + + // crds directory is optional + if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) { + return + } + + crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath)) + if !crdsDirValid { + return + } + + // Load chart and parse CRDs + chart, err := loader.Load(context.Background(), linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + /* Iterate over all the CRDs to check: + 1. It is a YAML file and not a template + 2. The API version is apiextensions.k8s.io + 3. The kind is CustomResourceDefinition + */ + for _, crd := range chart.CRDObjects() { + fileName := crd.Name + fpath = fileName + + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096) + for { + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if errors.Is(err, io.EOF) { + break + } + + // If YAML parsing fails here, it will always fail in the next block as well, so we should return here. + // This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct. + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + + if yamlStruct != nil { + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct)) + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct)) + } + } + } +} + +// Validation functions +func validateCrdsDir(crdsPath string) error { + fi, err := os.Stat(crdsPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateCrdAPIVersion(obj *k8sYamlStruct) error { + if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") { + return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'") + } + return nil +} + +func validateCrdKind(obj *k8sYamlStruct) error { + if obj.Kind != "CustomResourceDefinition" { + return fmt.Errorf("object kind is not 'CustomResourceDefinition'") + } + return nil +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/crds_test.go b/pkg/helm/intern/chart/v3/lint/rules/crds_test.go new file mode 100644 index 00000000..943fd87c --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/crds_test.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" +) + +const invalidCrdsDir = "./testdata/invalidcrdsdir" + +func TestInvalidCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: invalidCrdsDir} + Crds(&linter) + res := linter.Messages + + assert.Len(t, res, 1) + assert.ErrorContains(t, res[0].Err, "not a directory") +} + +// multi-document YAML with empty documents would panic +func TestCrdWithEmptyDocument(t *testing.T) { + chartDir := t.TempDir() + + os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte( + `apiVersion: v1 +name: test +version: 0.1.0 +`), 0644) + + // CRD with comments before --- (creates empty document) + crdsDir := filepath.Join(chartDir, "crds") + os.Mkdir(crdsDir, 0755) + os.WriteFile(filepath.Join(crdsDir, "test.yaml"), []byte( + `# Comments create empty document +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: test.example.io +`), 0644) + + linter := support.Linter{ChartDir: chartDir} + Crds(&linter) + + assert.Len(t, linter.Messages, 0) +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/dependencies.go b/pkg/helm/intern/chart/v3/lint/rules/dependencies.go new file mode 100644 index 00000000..b1667497 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/dependencies.go @@ -0,0 +1,102 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/rules" + +import ( + "context" + "fmt" + "strings" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" +) + +// Dependencies runs lints against a chart's dependencies +// +// See https://github.com/helm/helm/issues/7910 +func Dependencies(linter *support.Linter) { + c, err := loader.LoadDir(context.Background(), linter.ChartDir) + if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c)) + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c)) + linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c)) +} + +func validateChartFormat(chartError error) error { + if chartError != nil { + return fmt.Errorf("unable to load chart\n\t%w", chartError) + } + return nil +} + +func validateDependencyInChartsDir(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Dependencies() { + dependencies[dep.Metadata.Name] = struct{}{} + } + for _, dep := range c.Metadata.Dependencies { + if _, ok := dependencies[dep.Name]; !ok { + missing = append(missing, dep.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependencyInMetadata(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Metadata.Dependencies { + dependencies[dep.Name] = struct{}{} + } + for _, dep := range c.Dependencies() { + if _, ok := dependencies[dep.Metadata.Name]; !ok { + missing = append(missing, dep.Metadata.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependenciesUnique(c *chart.Chart) (err error) { + dependencies := map[string]*chart.Dependency{} + shadowing := []string{} + + for _, dep := range c.Metadata.Dependencies { + key := dep.Name + if dep.Alias != "" { + key = dep.Alias + } + if dependencies[key] != nil { + shadowing = append(shadowing, key) + } + dependencies[key] = dep + } + if len(shadowing) > 0 { + err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ",")) + } + return err +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/dependencies_test.go b/pkg/helm/intern/chart/v3/lint/rules/dependencies_test.go new file mode 100644 index 00000000..465a40ca --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/dependencies_test.go @@ -0,0 +1,157 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package rules + +import ( + "path/filepath" + "testing" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" +) + +func chartWithBadDependencies() chart.Chart { + badChartDeps := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "sub2", + }, + { + Name: "sub3", + }, + }, + }, + } + + badChartDeps.SetDependencies( + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub1", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub2", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + ) + return badChartDeps +} + +func TestValidateDependencyInChartsDir(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInChartsDir(&c); err == nil { + t.Error("chart should have been flagged for missing deps in chart directory") + } +} + +func TestValidateDependencyInMetadata(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInMetadata(&c); err == nil { + t.Errorf("chart should have been flagged for missing deps in chart metadata") + } +} + +func TestValidateDependenciesUnique(t *testing.T) { + tests := []struct { + chart chart.Chart + }{ + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + }, + { + Name: "foo", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "bar", + }, + { + Name: "bar", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "baz", + }, + { + Name: "bar", + Alias: "baz", + }, + }, + }, + }}, + } + + for _, tt := range tests { + if err := validateDependenciesUnique(&tt.chart); err == nil { + t.Errorf("chart should have been flagged for dependency shadowing") + } + } +} + +func TestDependencies(t *testing.T) { + tmp := t.TempDir() + + c := chartWithBadDependencies() + err := chartutil.SaveDir(&c, tmp) + if err != nil { + t.Fatal(err) + } + linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)} + + Dependencies(&linter) + if l := len(linter.Messages); l != 2 { + t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l) + for i, msg := range linter.Messages { + t.Logf("Message: %d, Error: %#v", i, msg) + } + } +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/deprecations.go b/pkg/helm/intern/chart/v3/lint/rules/deprecations.go new file mode 100644 index 00000000..21a4e579 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/deprecations.go @@ -0,0 +1,94 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/rules" + +import ( + "fmt" + "strconv" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/deprecation" + kscheme "k8s.io/client-go/kubernetes/scheme" +) + +// deprecatedAPIError indicates than an API is deprecated in Kubernetes +type deprecatedAPIError struct { + Deprecated string + Message string +} + +func (e deprecatedAPIError) Error() string { + msg := e.Message + return msg +} + +func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error { + // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation + if resource.APIVersion == "" { + return nil + } + if resource.Kind == "" { + return nil + } + + if kubeVersion == nil { + kubeVersion = &common.DefaultCapabilities.KubeVersion + } + + kubeVersionMajor, err := strconv.Atoi(kubeVersion.Major) + if err != nil { + return err + } + kubeVersionMinor, err := strconv.Atoi(kubeVersion.Minor) + if err != nil { + return err + } + + runtimeObject, err := resourceToRuntimeObject(resource) + if err != nil { + // do not error for non-kubernetes resources + if runtime.IsNotRegisteredError(err) { + return nil + } + return err + } + + if !deprecation.IsDeprecated(runtimeObject, kubeVersionMajor, kubeVersionMinor) { + return nil + } + gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) + return deprecatedAPIError{ + Deprecated: gvk, + Message: deprecation.WarningMessage(runtimeObject), + } +} + +func resourceToRuntimeObject(resource *k8sYamlStruct) (runtime.Object, error) { + scheme := runtime.NewScheme() + kscheme.AddToScheme(scheme) + + gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind) + out, err := scheme.New(gvk) + if err != nil { + return nil, err + } + out.GetObjectKind().SetGroupVersionKind(gvk) + return out, nil +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/deprecations_test.go b/pkg/helm/intern/chart/v3/lint/rules/deprecations_test.go new file mode 100644 index 00000000..210ab353 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/deprecations_test.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/rules" + +import "testing" + +func TestValidateNoDeprecations(t *testing.T) { + deprecated := &k8sYamlStruct{ + APIVersion: "extensions/v1beta1", + Kind: "Deployment", + } + err := validateNoDeprecations(deprecated, nil) + if err == nil { + t.Fatal("Expected deprecated extension to be flagged") + } + depErr := err.(deprecatedAPIError) + if depErr.Message == "" { + t.Fatalf("Expected error message to be non-blank: %v", err) + } + + if err := validateNoDeprecations(&k8sYamlStruct{ + APIVersion: "v1", + Kind: "Pod", + }, nil); err != nil { + t.Errorf("Expected a v1 Pod to not be deprecated") + } +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/template.go b/pkg/helm/intern/chart/v3/lint/rules/template.go new file mode 100644 index 00000000..adef230b --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/template.go @@ -0,0 +1,354 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/api/validation" + apipath "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" + chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" + "github.com/werf/nelm/pkg/helm/pkg/engine" +) + +// Templates lints the templates in the Linter. +func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { + TemplatesWithKubeVersion(linter, values, namespace, nil) +} + +// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. +func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) { + TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) +} + +// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not. +func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) { + fpath := "templates/" + templatesPath := filepath.Join(linter.ChartDir, fpath) + + // Templates directory is optional for now + templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath)) + if !templatesDirExists { + return + } + + validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath)) + if !validTemplatesDir { + return + } + + // Load chart and parse templates + chart, err := loader.Load(context.Background(), linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + options := common.ReleaseOptions{ + Name: "test-release", + Namespace: namespace, + } + + caps := common.DefaultCapabilities.Copy() + if kubeVersion != nil { + caps.KubeVersion = *kubeVersion + } + + // lint ignores import-values + // See https://github.com/helm/helm/issues/9658 + if err := chartutil.ProcessDependencies(chart, values); err != nil { + return + } + + cvals, err := util.CoalesceValues(chart, values) + if err != nil { + return + } + + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + if err != nil { + linter.RunLinterRule(support.ErrorSev, fpath, err) + return + } + var e engine.Engine + e.LintMode = true + renderedContentMap, err := e.Render(context.Background(), chart, valuesToRender) + + renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !renderOk { + return + } + + /* Iterate over all the templates to check: + - It is a .yaml file + - All the values in the template file is defined + - {{}} include | quote + - Generated content is a valid Yaml file + - Metadata.Namespace is not set + */ + for _, template := range chart.Templates { + fileName := template.Name + fpath = fileName + + linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) + + // We only apply the following lint rules to yaml files + if !isYamlFileExtension(fileName) { + continue + } + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 + // Check that all the templates have a matching value + // linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 + // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) + + renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] + if strings.TrimSpace(renderedContent) != "" { + linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent)) + + decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) + + // Lint all resources if the file contains multiple documents separated by --- + for { + // Even though k8sYamlStruct only defines a few fields, an error in any other + // key will be raised as well + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if errors.Is(err, io.EOF) { + break + } + + // If YAML linting fails here, it will always fail in the next block as well, so we should return here. + // fix https://github.com/helm/helm/issues/11391 + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + if yamlStruct != nil { + // NOTE: set to warnings to allow users to support out-of-date kubernetes + // Refs https://github.com/helm/helm/issues/8596 + linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) + linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) + + linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) + linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) + } + } + } + } +} + +// validateTopIndentLevel checks that the content does not start with an indent level > 0. +// +// This error can occur when a template accidentally inserts space. It can cause +// unpredictable errors depending on whether the text is normalized before being passed +// into the YAML parser. So we trap it here. +// +// See https://github.com/helm/helm/issues/8467 +func validateTopIndentLevel(content string) error { + // Read lines until we get to a non-empty one + scanner := bufio.NewScanner(bytes.NewBufferString(content)) + for scanner.Scan() { + line := scanner.Text() + // If line is empty, skip + if strings.TrimSpace(line) == "" { + continue + } + // If it starts with one or more spaces, this is an error + if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { + return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line) + } + // Any other condition passes. + return nil + } + return scanner.Err() +} + +// Validation functions +func templatesDirExists(templatesPath string) error { + _, err := os.Stat(templatesPath) + if errors.Is(err, os.ErrNotExist) { + return errors.New("directory does not exist") + } + return nil +} + +func validateTemplatesDir(templatesPath string) error { + fi, err := os.Stat(templatesPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateAllowedExtension(fileName string) error { + ext := filepath.Ext(fileName) + validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"} + + if slices.Contains(validExtensions, ext) { + return nil + } + + return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) +} + +func validateYamlContent(err error) error { + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + return nil +} + +// validateMetadataName uses the correct validation function for the object +// Kind, or if not set, defaults to the standard definition of a subdomain in +// DNS (RFC 1123), used by most resources. +func validateMetadataName(obj *k8sYamlStruct) error { + fn := validateMetadataNameFunc(obj) + allErrs := field.ErrorList{} + for _, msg := range fn(obj.Metadata.Name, false) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg)) + } + if len(allErrs) > 0 { + return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q: %w", obj.Metadata.Name, allErrs.ToAggregate()) + } + return nil +} + +// validateMetadataNameFunc will return a name validation function for the +// object kind, if defined below. +// +// Rules should match those set in the various api validations: +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274 +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39 +// ... +// +// Implementing here to avoid importing k/k. +// +// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object +// kinds that don't have special requirements, so is the most likely to work if +// new kinds are added. +func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { + switch strings.ToLower(obj.Kind) { + case "pod", "node", "secret", "endpoints", "resourcequota", // core + "controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps + "autoscaler", // autoscaler + "cronjob", "job", // batch + "lease", // coordination + "endpointslice", // discovery + "networkpolicy", "ingress", // networking + "podsecuritypolicy", // policy + "priorityclass", // scheduling + "podpreset", // settings + "storageclass", "volumeattachment", "csinode": // storage + return validation.NameIsDNSSubdomain + case "service": + return validation.NameIsDNS1035Label + case "namespace": + return validation.ValidateNamespaceName + case "serviceaccount": + return validation.ValidateServiceAccountName + case "certificatesigningrequest": + // No validation. + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140 + return func(_ string, _ bool) []string { return nil } + case "role", "clusterrole", "rolebinding", "clusterrolebinding": + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 + return func(name string, _ bool) []string { + return apipath.IsValidPathSegmentName(name) + } + default: + return validation.NameIsDNSSubdomain + } +} + +// validateMatchSelector ensures that template specs have a selector declared. +// See https://github.com/helm/helm/issues/1990 +func validateMatchSelector(yamlStruct *k8sYamlStruct, manifest string) error { + switch yamlStruct.Kind { + case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet": + // verify that matchLabels or matchExpressions is present + if !strings.Contains(manifest, "matchLabels") && !strings.Contains(manifest, "matchExpressions") { + return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name) + } + } + return nil +} + +func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error { + if yamlStruct.Kind == "List" { + m := struct { + Items []struct { + Metadata struct { + Annotations map[string]string + } + } + }{} + + if err := yaml.Unmarshal([]byte(manifest), &m); err != nil { + return validateYamlContent(err) + } + + for _, i := range m.Items { + if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok { + return errors.New("annotation 'helm.sh/resource-policy' within List objects are ignored") + } + } + } + return nil +} + +func isYamlFileExtension(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + return ext == ".yaml" || ext == ".yml" +} + +// k8sYamlStruct stubs a Kubernetes YAML file. +type k8sYamlStruct struct { + APIVersion string `json:"apiVersion"` + Kind string + Metadata k8sYamlMetadata +} + +type k8sYamlMetadata struct { + Namespace string + Name string +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/template_test.go b/pkg/helm/intern/chart/v3/lint/rules/template_test.go new file mode 100644 index 00000000..514579c9 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/template_test.go @@ -0,0 +1,467 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +const templateTestBasedir = "./testdata/albatross" + +func TestValidateAllowedExtension(t *testing.T) { + var failTest = []string{"/foo", "/test.toml"} + for _, test := range failTest { + err := validateAllowedExtension(test) + if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") { + t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test) + } + } + var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"} + for _, test := range successTest { + err := validateAllowedExtension(test) + if err != nil { + t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error()) + } + } +} + +var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} + +const namespace = "testNamespace" +const strict = false + +func TestTemplateParsing(t *testing.T) { + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected one error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("Unexpected error: %s", res[0]) + } +} + +var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml") +var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored") + +// Test a template with all the existing features: +// namespaces, partial templates +func TestTemplateIntegrationHappyPath(t *testing.T) { + // Rename file so it gets ignored by the linter + os.Rename(wrongTemplatePath, ignoredTemplatePath) + defer os.Rename(ignoredTemplatePath, wrongTemplatePath) + + linter := support.Linter{ChartDir: templateTestBasedir} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 0 { + t.Fatalf("Expected no error, got %d, %v", len(res), res) + } +} + +func TestMultiTemplateFail(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} + Templates(&linter, values, namespace, strict) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected 1 error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") { + t.Errorf("Unexpected error: %s", res[0].Err) + } +} + +func TestValidateMetadataName(t *testing.T) { + tests := []struct { + obj *k8sYamlStruct + wantErr bool + }{ + // Most kinds use IsDNS1123Subdomain. + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true}, + + // Service uses IsDNS1035Label. + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + + // Namespace uses IsDNS1123Label. + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false}, + + // CertificateSigningRequest has no validation. + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false}, + + // RBAC uses path validation. + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + + // Unknown Kind + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + + // No kind + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) { + if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr { + t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDeprecatedAPIFails(t *testing.T) { + modTime := time.Now() + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "failapi", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/baddeployment.yaml", + ModTime: modTime, + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + }, + { + Name: "templates/goodsecret.yaml", + ModTime: modTime, + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l != 1 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 1 lint error, got %d", l) + } + + err := linter.Messages[0].Err.(deprecatedAPIError) + if err.Deprecated != "apps/v1beta1 Deployment" { + t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) + } +} + +const manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + myval1: {{default "val" .Values.mymap.key1 }} + myval2: {{default "val" .Values.mymap.key2 }} +` + +// TestStrictTemplateParsingMapError is a regression test. +// +// The template engine should not produce an error when a map in values.yaml does +// not contain all possible keys. +// +// See https://github.com/helm/helm/issues/7483 +func TestStrictTemplateParsingMapError(t *testing.T) { + + ch := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "regression7483", + APIVersion: "v2", + Version: "0.1.0", + }, + Values: map[string]interface{}{ + "mymap": map[string]string{ + "key1": "val1", + }, + }, + Templates: []*common.File{ + { + Name: "templates/configmap.yaml", + ModTime: time.Now(), + Data: []byte(manifest), + }, + }, + } + dir := t.TempDir() + if err := chartutil.SaveDir(&ch, dir); err != nil { + t.Fatal(err) + } + linter := &support.Linter{ + ChartDir: filepath.Join(dir, ch.Metadata.Name), + } + Templates(linter, ch.Values, namespace, strict) + if len(linter.Messages) != 0 { + t.Errorf("expected zero messages, got %d", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %q", i, msg) + } + } +} + +func TestValidateMatchSelector(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "apps/v1", + Kind: "Deployment", + Metadata: k8sYamlMetadata{ + Name: "mydeployment", + }, + } + manifest := ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchExpressions: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err == nil { + t.Error("expected Deployment with no selector to fail") + } +} + +func TestValidateTopIndentLevel(t *testing.T) { + for doc, shouldFail := range map[string]bool{ + // Should not fail + "\n\n\n\t\n \t\n": false, + "apiVersion:foo\n bar:baz": false, + "\n\n\napiVersion:foo\n\n\n": false, + // Should fail + " apiVersion:foo": true, + "\n\n apiVersion:foo\n\n": true, + } { + if err := validateTopIndentLevel(doc); (err == nil) == shouldFail { + t.Errorf("Expected %t for %q", shouldFail, doc) + } + } + +} + +// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments +// See https://github.com/helm/helm/issues/8621 +func TestEmptyWithCommentsManifests(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "emptymanifests", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/empty-with-comments.yaml", + ModTime: time.Now(), + Data: []byte("#@formatter:off\n"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates(&linter, values, namespace, strict) + if l := len(linter.Messages); l > 0 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 0 lint errors, got %d", l) + } +} +func TestValidateListAnnotations(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "v1", + Kind: "List", + Metadata: k8sYamlMetadata{ + Name: "list", + }, + } + manifest := ` +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ConfigMap + metadata: + annotations: + helm.sh/resource-policy: keep +` + + if err := validateListAnnotations(md, manifest); err == nil { + t.Fatal("expected list with nested keep annotations to fail") + } + + manifest = ` +apiVersion: v1 +kind: List +metadata: + annotations: + helm.sh/resource-policy: keep +items: + - apiVersion: v1 + kind: ConfigMap +` + + if err := validateListAnnotations(md, manifest); err != nil { + t.Fatalf("List objects keep annotations should pass. got: %s", err) + } +} + +func TestIsYamlFileExtension(t *testing.T) { + tests := []struct { + filename string + expected bool + }{ + {"test.yaml", true}, + {"test.yml", true}, + {"test.txt", false}, + {"test", false}, + } + + for _, test := range tests { + result := isYamlFileExtension(test.filename) + if result != test.expected { + t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected) + } + } + +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/Chart.yaml new file mode 100644 index 00000000..5e1ed515 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: albatross +description: testing chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/helm/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl b/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/templates/_helpers.tpl similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/albatross/templates/_helpers.tpl rename to pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/templates/_helpers.tpl diff --git a/pkg/helm/pkg/lint/rules/testdata/albatross/templates/fail.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/templates/fail.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/albatross/templates/fail.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/templates/fail.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/albatross/templates/svc.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/templates/svc.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/albatross/templates/svc.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/templates/svc.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/albatross/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/albatross/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/albatross/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml new file mode 100644 index 00000000..8a598473 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/anotherbadchartfile/Chart.yaml @@ -0,0 +1,15 @@ +name: "some-chart" +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 72445e2 +home: "" +type: application +appVersion: 72225e2 +icon: "https://some-url.com/icon.jpeg" +dependencies: + - name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/helm/pkg/lint/rules/testdata/badchartfile/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badchartfile/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badchartfile/Chart.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/badchartfile/Chart.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/badchartfile/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badchartfile/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badchartfile/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/badchartfile/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/badchartname/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badchartname/Chart.yaml new file mode 100644 index 00000000..41f45235 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/badchartname/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: "../badchartname" +type: application diff --git a/pkg/helm/pkg/lint/rules/testdata/badchartname/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badchartname/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badchartname/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/badchartname/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml new file mode 100644 index 00000000..3bf00739 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: badcrdfile +type: application +icon: http://riverrun.io diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml new file mode 100644 index 00000000..46891605 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml @@ -0,0 +1,2 @@ +apiVersion: bad.k8s.io/v1beta1 +kind: CustomResourceDefinition diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml new file mode 100644 index 00000000..523b97f8 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml @@ -0,0 +1,2 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: NotACustomResourceDefinition diff --git a/pkg/helm/pkg/chartutil/testdata/joonix/charts/.gitkeep b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/templates/.gitkeep similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/joonix/charts/.gitkeep rename to pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/templates/.gitkeep diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/values.yaml new file mode 100644 index 00000000..2fffc771 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/badcrdfile/values.yaml @@ -0,0 +1 @@ +# Default values for badcrdfile. diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml new file mode 100644 index 00000000..aace27e2 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +name: badvaluesfile +description: A Helm chart for Kubernetes +version: 0.0.1 +home: "" +icon: http://riverrun.io diff --git a/pkg/helm/pkg/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/badvaluesfile/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badvaluesfile/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/badvaluesfile/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/Chart.yaml new file mode 100644 index 00000000..bf8f5e30 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: goodone +description: good testing chart +version: 199.44.12345-Alpha.1+cafe009 +icon: http://riverrun.io diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/crds/test-crd.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/crds/test-crd.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/goodone/templates/goodone.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/templates/goodone.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/goodone/templates/goodone.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/templates/goodone.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/goodone/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/goodone/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/goodone/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml new file mode 100644 index 00000000..0fd58d1d --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidchartfile/Chart.yaml @@ -0,0 +1,6 @@ +name: some-chart +apiVersion: v2 +apiVersion: v1 +description: A Helm chart for Kubernetes +version: 1.3.0 +icon: http://example.com diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidchartfile/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/invalidchartfile/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml new file mode 100644 index 00000000..0f6d1ee9 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: invalidcrdsdir +type: application +icon: http://riverrun.io diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/repository/test-name-charts.txt b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/crds similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm home with space/helm/repository/test-name-charts.txt rename to pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/crds diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml new file mode 100644 index 00000000..6b1611a6 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/invalidcrdsdir/values.yaml @@ -0,0 +1 @@ +# Default values for invalidcrdsdir. diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore b/pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-only-crds/.helmignore rename to pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/.helmignore diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml new file mode 100644 index 00000000..d46b98cb --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/Chart.yaml @@ -0,0 +1,25 @@ +apiVersion: v3 +name: test +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +icon: https://riverrun.io \ No newline at end of file diff --git a/pkg/helm/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/templates/bad.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/malformed-template/templates/bad.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/templates/bad.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/malformed-template/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/malformed-template/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/malformed-template/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml new file mode 100644 index 00000000..bfb580be --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/multi-template-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: multi-template-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/helm/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml new file mode 100644 index 00000000..2a29c33f --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: v3-fail +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application and it is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/_helpers.tpl similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/_helpers.tpl rename to pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/_helpers.tpl diff --git a/pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/deployment.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/deployment.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/deployment.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/ingress.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/ingress.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/ingress.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/service.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/v3-fail/templates/service.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/templates/service.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/v3-fail/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/v3-fail/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/v3-fail/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml new file mode 100644 index 00000000..fa15eaba --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v3 +name: withsubchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" +icon: http://riverrun.io + +dependencies: + - name: subchart + version: 0.1.16 + repository: "file://../subchart" + import-values: + - child: subchart + parent: subchart + diff --git a/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml new file mode 100644 index 00000000..35b13e70 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +name: subchart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/helm/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/values.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/withsubchart/charts/subchart/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/charts/subchart/values.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/templates/mainchart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/withsubchart/templates/mainchart.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/templates/mainchart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/values.yaml b/pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/values.yaml rename to pkg/helm/intern/chart/v3/lint/rules/testdata/withsubchart/values.yaml diff --git a/pkg/helm/intern/chart/v3/lint/rules/values.go b/pkg/helm/intern/chart/v3/lint/rules/values.go new file mode 100644 index 00000000..ae00a2d6 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/values.go @@ -0,0 +1,84 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" +) + +// ValuesWithOverrides tests the values.yaml file. +// +// If a schema is present in the chart, values are tested against that. Otherwise, +// they are only tested for well-formedness. +// +// If additional values are supplied, they are coalesced into the values in values.yaml. +func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) { + file := "values.yaml" + vf := filepath.Join(linter.ChartDir, file) + fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) + + if !fileExists { + return + } + + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation)) +} + +func validateValuesFileExistence(valuesPath string) error { + _, err := os.Stat(valuesPath) + if err != nil { + return fmt.Errorf("file does not exist") + } + return nil +} + +func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error { + values, err := common.ReadValuesFile(valuesPath) + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + + // Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top + // level values against the top-level expectations. Subchart values are not linted. + // We could change that. For now, though, we retain that strategy, and thus can + // coalesce tables (like reuse-values does) instead of doing the full chart + // CoalesceValues + coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = util.CoalesceTables(coalescedValues, values) + + ext := filepath.Ext(valuesPath) + schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" + schema, err := os.ReadFile(schemaPath) + if len(schema) == 0 { + return nil + } + if err != nil { + return err + } + + if !skipSchemaValidation { + return util.ValidateAgainstSingleSchema(coalescedValues, schema) + } + + return nil +} diff --git a/pkg/helm/intern/chart/v3/lint/rules/values_test.go b/pkg/helm/intern/chart/v3/lint/rules/values_test.go new file mode 100644 index 00000000..a2a5345d --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/rules/values_test.go @@ -0,0 +1,183 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/intern/test/ensure" +) + +var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") + +const testSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "helm values test schema", + "type": "object", + "additionalProperties": false, + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "description": "Your username", + "type": "string" + }, + "password": { + "description": "Your password", + "type": "string" + } + } +} +` + +func TestValidateValuesYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) + defer os.Remove(nonExistingValuesFilePath) + + err := validateValuesFileExistence(nonExistingValuesFilePath) + if err == nil { + t.Errorf("validateValuesFileExistence to return a linter error, got no error") + } +} + +func TestValidateValuesFileWellFormed(t *testing.T) { + badYaml := ` + not:well[]{}formed + ` + tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil { + t.Fatal("expected values file to fail parsing") + } +} + +func TestValidateValuesFileSchema(t *testing.T) { + yaml := "username: admin\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFileSchemaFailure(t *testing.T) { + // 1234 is an int, not a string. This should fail. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}, false) + if err == nil { + t.Fatal("expected values file to fail parsing") + } + + assert.Contains(t, err.Error(), "- at '/username': got number, want string") +} + +func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) { + // 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}, true) + if err != nil { + t.Fatal("expected values file to pass parsing because of skipSchemaValidation") + } +} + +func TestValidateValuesFileSchemaOverrides(t *testing.T) { + yaml := "username: admin" + overrides := map[string]interface{}{ + "password": "swordfish", + } + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, overrides, false); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFile(t *testing.T) { + tests := []struct { + name string + yaml string + overrides map[string]interface{} + errorMessage string + }{ + { + name: "value added", + yaml: "username: admin", + overrides: map[string]interface{}{"password": "swordfish"}, + }, + { + name: "value not overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser"}, + errorMessage: "- at '/password': got null, want string", + }, + { + name: "value overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpdir := ensure.TempFile(t, "values.yaml", []byte(tt.yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, tt.overrides, false) + + switch { + case err != nil && tt.errorMessage == "": + t.Errorf("Failed validation with %s", err) + case err == nil && tt.errorMessage != "": + t.Error("expected values file to fail parsing") + case err != nil && tt.errorMessage != "": + assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error") + } + }) + } +} + +func createTestingSchema(t *testing.T, dir string) string { + t.Helper() + schemafile := filepath.Join(dir, "values.schema.json") + if err := os.WriteFile(schemafile, []byte(testSchema), 0700); err != nil { + t.Fatalf("Failed to write schema to tmpdir: %s", err) + } + return schemafile +} diff --git a/pkg/helm/intern/chart/v3/lint/support/doc.go b/pkg/helm/intern/chart/v3/lint/support/doc.go new file mode 100644 index 00000000..04446bb5 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/support/doc.go @@ -0,0 +1,23 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package support contains tools for linting charts. + +Linting is the process of testing charts for errors or warnings regarding +formatting, compilation, or standards compliance. +*/ +package support // import "github.com/werf/nelm/pkg/helm/intern/chart/v3/lint/support" diff --git a/pkg/helm/intern/chart/v3/lint/support/message.go b/pkg/helm/intern/chart/v3/lint/support/message.go new file mode 100644 index 00000000..5efbc7a6 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/support/message.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import "fmt" + +// Severity indicates the severity of a Message. +const ( + // UnknownSev indicates that the severity of the error is unknown, and should not stop processing. + UnknownSev = iota + // InfoSev indicates information, for example missing values.yaml file + InfoSev + // WarningSev indicates that something does not meet code standards, but will likely function. + WarningSev + // ErrorSev indicates that something will not likely function. + ErrorSev +) + +// sev matches the *Sev states. +var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} + +// Linter encapsulates a linting run of a particular chart. +type Linter struct { + Messages []Message + // The highest severity of all the failing lint rules + HighestSeverity int + ChartDir string +} + +// Message describes an error encountered while linting. +type Message struct { + // Severity is one of the *Sev constants + Severity int + Path string + Err error +} + +func (m Message) Error() string { + return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) +} + +// NewMessage creates a new Message struct +func NewMessage(severity int, path string, err error) Message { + return Message{Severity: severity, Path: path, Err: err} +} + +// RunLinterRule returns true if the validation passed +func (l *Linter) RunLinterRule(severity int, path string, err error) bool { + // severity is out of bound + if severity < 0 || severity >= len(sev) { + return false + } + + if err != nil { + l.Messages = append(l.Messages, NewMessage(severity, path, err)) + + if severity > l.HighestSeverity { + l.HighestSeverity = severity + } + } + return err == nil +} diff --git a/pkg/helm/intern/chart/v3/lint/support/message_test.go b/pkg/helm/intern/chart/v3/lint/support/message_test.go new file mode 100644 index 00000000..ce5b5e42 --- /dev/null +++ b/pkg/helm/intern/chart/v3/lint/support/message_test.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import ( + "errors" + "testing" +) + +var errLint = errors.New("lint failed") + +func TestRunLinterRule(t *testing.T) { + var tests = []struct { + Severity int + LintError error + ExpectedMessages int + ExpectedReturn bool + ExpectedHighestSeverity int + }{ + {InfoSev, errLint, 1, false, InfoSev}, + {WarningSev, errLint, 2, false, WarningSev}, + {ErrorSev, errLint, 3, false, ErrorSev}, + // No error so it returns true + {ErrorSev, nil, 3, true, ErrorSev}, + // Retains highest severity + {InfoSev, errLint, 4, false, ErrorSev}, + // Invalid severity values + {4, errLint, 4, false, ErrorSev}, + {22, errLint, 4, false, ErrorSev}, + {-1, errLint, 4, false, ErrorSev}, + } + + linter := Linter{} + for _, test := range tests { + isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError) + if len(linter.Messages) != test.ExpectedMessages { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) + } + + if linter.HighestSeverity != test.ExpectedHighestSeverity { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) + } + + if isValid != test.ExpectedReturn { + t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) + } + } +} + +func TestMessage(t *testing.T) { + m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")} + if m.Error() != "[ERROR] Chart.yaml: Foo" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{WarningSev, "templates/", errors.New("Bar")} + if m.Error() != "[WARNING] templates/: Bar" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")} + if m.Error() != "[INFO] templates/rc.yaml: FooBar" { + t.Errorf("Unexpected output: %s", m.Error()) + } +} diff --git a/pkg/helm/intern/chart/v3/loader/archive.go b/pkg/helm/intern/chart/v3/loader/archive.go new file mode 100644 index 00000000..b097c84d --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/archive.go @@ -0,0 +1,75 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "os" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" +) + +// FileLoader loads a chart from a file +type FileLoader string + +// Load loads a chart +func (l FileLoader) Load(ctx context.Context) (*chart.Chart, error) { + return LoadFile(ctx, string(l)) +} + +// LoadFile loads from an archive file. +func LoadFile(ctx context.Context, name string) (*chart.Chart, error) { + if fi, err := os.Stat(name); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory") + } + + raw, err := os.Open(name) + if err != nil { + return nil, err + } + defer raw.Close() + + err = archive.EnsureArchive(name, raw) + if err != nil { + return nil, err + } + + c, err := LoadArchive(ctx, raw) + if err != nil { + if errors.Is(err, gzip.ErrHeader) { + return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) + } + } + return c, err +} + +// LoadArchive loads from a reader containing a compressed tar archive. +func LoadArchive(ctx context.Context, in io.Reader) (*chart.Chart, error) { + files, err := archive.LoadArchiveFiles(in) + if err != nil { + return nil, err + } + + return LoadFiles(ctx, files) +} diff --git a/pkg/helm/intern/chart/v3/loader/chart_metadata.go b/pkg/helm/intern/chart/v3/loader/chart_metadata.go new file mode 100644 index 00000000..04d8f3f1 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/chart_metadata.go @@ -0,0 +1,37 @@ +package loader + +import chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + +type autosetChartMetadataOptions struct { + OverrideAppVersion string + DefaultAPIVersion string + DefaultName string + DefaultVersion string +} + +func autosetChartMetadata(metadataIn *chart.Metadata, opts autosetChartMetadataOptions) *chart.Metadata { + var metadata *chart.Metadata + if metadataIn == nil { + metadata = &chart.Metadata{} + } else { + metadata = metadataIn + } + + if metadata.APIVersion == "" { + metadata.APIVersion = opts.DefaultAPIVersion + } + + if metadata.Name == "" { + metadata.Name = opts.DefaultName + } + + if opts.OverrideAppVersion != "" { + metadata.AppVersion = opts.OverrideAppVersion + } + + if metadata.Version == "" { + metadata.Version = opts.DefaultVersion + } + + return metadata +} diff --git a/pkg/helm/intern/chart/v3/loader/directory.go b/pkg/helm/intern/chart/v3/loader/directory.go new file mode 100644 index 00000000..85ce4800 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/directory.go @@ -0,0 +1,123 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/sympath" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + "github.com/werf/nelm/pkg/helm/pkg/ignore" +) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// DirLoader loads a chart from a directory +type DirLoader string + +// Load loads the chart +func (l DirLoader) Load(ctx context.Context) (*chart.Chart, error) { + return LoadDir(ctx, string(l)) +} + +// LoadDir loads from a directory. +// +// This loads charts only from directories. +func LoadDir(ctx context.Context, dir string) (*chart.Chart, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + // Just used for errors. + c := &chart.Chart{} + + rules := ignore.Empty() + ifile := filepath.Join(topdir, ignore.HelmIgnore) + if _, err := os.Stat(ifile); err == nil { + r, err := ignore.ParseFile(ifile) + if err != nil { + return c, err + } + rules = r + } + rules.AddDefaults() + + files := []*archive.BufferedFile{} + topdir += string(filepath.Separator) + + walk := func(name string, fi os.FileInfo, err error) error { + n := strings.TrimPrefix(name, topdir) + if n == "" { + // No need to process top level. Avoid bug with helmignore .* matching + // empty names. See issue 1779. + return nil + } + + // Normalize to / since it will also work on Windows + n = filepath.ToSlash(n) + + if err != nil { + return err + } + if fi.IsDir() { + // Directory-based ignore rules should involve skipping the entire + // contents of that directory. + if rules.Ignore(n, fi) { + return filepath.SkipDir + } + return nil + } + + // If a .helmignore file matches, skip this file. + if rules.Ignore(n, fi) { + return nil + } + + // Irregular files include devices, sockets, and other uses of files that + // are not regular files. In Go they have a file mode type bit set. + // See https://golang.org/pkg/os/#FileMode for examples. + if !fi.Mode().IsRegular() { + return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) + } + + if fi.Size() > archive.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) + } + + data, err := os.ReadFile(name) + if err != nil { + return fmt.Errorf("error reading %s: %w", n, err) + } + + data = bytes.TrimPrefix(data, utf8bom) + + files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data}) + return nil + } + if err = sympath.Walk(topdir, walk); err != nil { + return c, err + } + + return LoadFiles(ctx, files) +} diff --git a/pkg/helm/intern/chart/v3/loader/load.go b/pkg/helm/intern/chart/v3/loader/load.go new file mode 100644 index 00000000..6cb44f8d --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/load.go @@ -0,0 +1,351 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "maps" + "os" + "path/filepath" + "slices" + "strings" + + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" + + "github.com/werf/common-go/pkg/secrets_manager" + nelmcommon "github.com/werf/nelm/pkg/common" + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + legacysecret "github.com/werf/nelm/pkg/legacy/secret" +) + +// ChartLoader loads a chart. +type ChartLoader interface { + Load(ctx context.Context) (*chart.Chart, error) +} + +// Loader returns a new ChartLoader appropriate for the given chart name +func Loader(name string) (ChartLoader, error) { + fi, err := os.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return DirLoader(name), nil + } + return FileLoader(name), nil +} + +// Load takes a string name, tries to resolve it to a file or directory, and then loads it. +// +// This is the preferred way to load a chart. It will discover the chart encoding +// and hand off to the appropriate chart reader. +// +// If a .helmignore file is present, the directory loader will skip loading any files +// matching it. But .helmignore is not evaluated when reading out of an archive. +func Load(ctx context.Context, name string) (*chart.Chart, error) { + l, err := Loader(name) + if err != nil { + return nil, err + } + return l.Load(ctx) +} + +// LoadFiles loads from in-memory files. +func LoadFiles(ctx context.Context, files []*archive.BufferedFile) (*chart.Chart, error) { + helmOpts := nelmcommon.HelmOptionsFromContext(ctx) + applyWerfExtensions := nelmcommon.HasHelmOptions(ctx) + + c := new(chart.Chart) + subcharts := make(map[string][]*archive.BufferedFile) + var subChartsKeys []string + + if applyWerfExtensions { + c.SecretsRuntimeData = legacysecret.NewSecretsRuntimeData() + } + + // do not rely on assumed ordering of files in the chart and crash + // if Chart.yaml was not coming early enough to initialize metadata + for _, f := range files { + c.Raw = append(c.Raw, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + if f.Name == "Chart.yaml" { + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { + return c, fmt.Errorf("cannot load Chart.yaml: %w", err) + } + // While the documentation says the APIVersion is required, in practice there + // are cases where that's not enforced. Since this package set is for v3 charts, + // when this function is used v3 is automatically added when not present. + if c.Metadata.APIVersion == "" { + c.Metadata.APIVersion = chart.APIVersionV3 + } + c.ModTime = f.ModTime + } + } + for _, f := range files { + switch { + case f.Name == "Chart.yaml": + continue + case f.Name == "Chart.lock": + c.Lock = new(chart.Lock) + if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { + return c, fmt.Errorf("cannot load Chart.lock: %w", err) + } + case f.Name == "values.yaml": + values, err := LoadValues(bytes.NewReader(f.Data)) + if err != nil { + return c, fmt.Errorf("cannot load values.yaml: %w", err) + } + c.Values = values + case f.Name == "values.schema.json": + c.Schema = f.Data + c.SchemaModTime = f.ModTime + + case strings.HasPrefix(f.Name, "templates/"): + c.Templates = append(c.Templates, &chartcommon.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime}) + case strings.HasPrefix(f.Name, "charts/"): + if filepath.Ext(f.Name) == ".prov" { + c.Files = append(c.Files, &chartcommon.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime}) + continue + } + + fname := strings.TrimPrefix(f.Name, "charts/") + cname := strings.SplitN(fname, "/", 2)[0] + if slices.Index(subChartsKeys, cname) == -1 { + subChartsKeys = append(subChartsKeys, cname) + } + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data}) + case applyWerfExtensions && strings.HasPrefix(f.Name, "ts/") && !strings.HasPrefix(f.Name, "ts/node_modules/"): + c.RuntimeFiles = append(c.RuntimeFiles, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + default: + c.Files = append(c.Files, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + } + } + + if applyWerfExtensions { + switch helmOpts.ChartLoadOpts.ChartType { + case nelmcommon.LegacyChartTypeBundle: + c.ExtraValues = helmOpts.ChartLoadOpts.ExtraValues + + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + CustomSecretValueFiles: helmOpts.ChartLoadOpts.SecretValuesFiles, + LoadFromLocalFilesystem: true, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + + if helmOpts.ChartLoadOpts.DefaultValuesDisable { + c.Values = nil + } + case nelmcommon.LegacyChartTypeChart: + c.ExtraValues = helmOpts.ChartLoadOpts.ExtraValues + + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + CustomSecretValueFiles: helmOpts.ChartLoadOpts.SecretValuesFiles, + LoadFromLocalFilesystem: nelmcommon.ChartFileReader == nil, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + + c.Metadata = autosetChartMetadata( + c.Metadata, + autosetChartMetadataOptions{ + DefaultAPIVersion: helmOpts.ChartLoadOpts.DefaultChartAPIVersion, + DefaultName: helmOpts.ChartLoadOpts.DefaultChartName, + DefaultVersion: helmOpts.ChartLoadOpts.DefaultChartVersion, + OverrideAppVersion: helmOpts.ChartLoadOpts.ChartAppVersion, + }, + ) + + c.Templates = append(c.Templates, &chartcommon.File{Name: "templates/_werf_helpers.tpl"}) + + if helmOpts.ChartLoadOpts.DefaultValuesDisable { + c.Values = nil + } + case nelmcommon.LegacyChartTypeSubchart: + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + LoadFromLocalFilesystem: nelmcommon.ChartFileReader == nil, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + case nelmcommon.LegacyChartTypeChartStub: + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + LoadFromLocalFilesystem: true, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + + c.Metadata = autosetChartMetadata( + c.Metadata, + autosetChartMetadataOptions{ + DefaultAPIVersion: chart.APIVersionV3, + DefaultName: "stubchartname", + DefaultVersion: "1.0.0", + }, + ) + + c.Templates = append(c.Templates, &chartcommon.File{Name: "templates/_werf_helpers.tpl"}) + default: + panic("unexpected type") + } + } + + if c.Metadata == nil { + return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck + } + + if err := c.Validate(); err != nil { + return c, err + } + + helmOpts.ChartLoadOpts.ChartType = nelmcommon.LegacyChartTypeSubchart + ctx = nelmcommon.ContextWithHelmOptions(ctx, helmOpts) + + for n, files := range subcharts { + var sc *chart.Chart + var err error + switch { + case strings.IndexAny(n, "_.") == 0: + continue + case filepath.Ext(n) == ".tgz": + file := files[0] + if file.Name != n { + return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name) + } + sc, err = LoadArchive(ctx, bytes.NewBuffer(file.Data)) + default: + buff := make([]*archive.BufferedFile, 0, len(files)) + for _, f := range files { + parts := strings.SplitN(f.Name, "/", 2) + if len(parts) < 2 { + continue + } + f.Name = parts[1] + buff = append(buff, f) + } + sc, err = LoadFiles(ctx, buff) + } + + if err != nil { + return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err) + } + c.AddDependency(sc) + } + + return c, nil +} + +func convertBufferedFiles(files []*archive.BufferedFile) []*nelmcommon.BufferedFile { + var res []*nelmcommon.BufferedFile + for _, f := range files { + res = append(res, &nelmcommon.BufferedFile{Name: f.Name, Data: f.Data}) + } + + return res +} + +// LoadValues loads values from a reader. +// +// The reader is expected to contain one or more YAML documents, the values of which are merged. +// And the values can be either a chart's default values or user-supplied values. +func LoadValues(data io.Reader) (map[string]interface{}, error) { + values := map[string]interface{}{} + reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + for { + currentMap := map[string]interface{}{} + raw, err := reader.Read() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("error reading yaml document: %w", err) + } + if err := yaml.Unmarshal(raw, ¤tMap); err != nil { + return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err) + } + values = MergeMaps(values, currentMap) + } + return values, nil +} + +// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used. +// If the value is a map, the maps will be merged recursively. +func MergeMaps(a, b map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(a)) + maps.Copy(out, a) + for k, v := range b { + if v, ok := v.(map[string]interface{}); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]interface{}); ok { + out[k] = MergeMaps(bv, v) + continue + } + } + } + out[k] = v + } + return out +} diff --git a/pkg/helm/intern/chart/v3/loader/load_test.go b/pkg/helm/intern/chart/v3/loader/load_test.go new file mode 100644 index 00000000..19f4a61a --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/load_test.go @@ -0,0 +1,727 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "errors" + "io" + "log" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + "time" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" +) + +func TestLoadDir(t *testing.T) { + l, err := Loader("testdata/frobnitz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadDirWithDevNull(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test only works on unix systems with /dev/null present") + } + + l, err := Loader("testdata/frobnitz_with_dev_null") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + if _, err := l.Load(context.Background()); err == nil { + t.Errorf("packages with an irregular file (/dev/null) should not load") + } +} + +func TestLoadDirWithSymlink(t *testing.T) { + sym := filepath.Join("..", "LICENSE") + link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE") + + if err := os.Symlink(sym, link); err != nil { + t.Fatal(err) + } + + defer os.Remove(link) + + l, err := Loader("testdata/frobnitz_with_symlink") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestBomTestData(t *testing.T) { + testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} + for _, file := range testFiles { + data, err := os.ReadFile("testdata/" + file) + if err != nil || !bytes.HasPrefix(data, utf8bom) { + t.Errorf("Test file has no BOM or is invalid: testdata/%s", file) + } + } + + archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + unzipped, err := gzip.NewReader(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + defer unzipped.Close() + for _, testFile := range testFiles { + data := make([]byte, 3) + err := unzipped.Reset(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + tr := tar.NewReader(unzipped) + for { + file, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + if file != nil && strings.EqualFold(file.Name, testFile) { + _, err := tr.Read(data) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } else { + break + } + } + } + if !bytes.Equal(data, utf8bom) { + t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile) + } + } +} + +func TestLoadDirWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadArchiveWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadFile(t *testing.T) { + l, err := Loader("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) +} + +func TestLoadFiles(t *testing.T) { + modTime := time.Now() + goodFiles := []*archive.BufferedFile{ + { + Name: "Chart.yaml", + ModTime: modTime, + Data: []byte(`apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +`), + }, + { + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), + }, + { + Name: "values.schema.json", + ModTime: modTime, + Data: []byte("type: Values"), + }, + { + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), + }, + } + + c, err := LoadFiles(context.Background(), goodFiles) + if err != nil { + t.Errorf("Expected good files to be loaded, got %v", err) + } + + if c.Name() != "frobnitz" { + t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name()) + } + + if c.Values["var"] != "some values" { + t.Error("Expected chart values to be populated with default values") + } + + if len(c.Raw) != 5 { + t.Errorf("Expected %d files, got %d", 5, len(c.Raw)) + } + + if !bytes.Equal(c.Schema, []byte("type: Values")) { + t.Error("Expected chart schema to be populated with default values") + } + + if len(c.Templates) != 2 { + t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) + } + + if _, err = LoadFiles(context.Background(), []*archive.BufferedFile{}); err == nil { + t.Fatal("Expected err to be non-nil") + } + if err.Error() != "Chart.yaml file is missing" { + t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) + } +} + +// Test the order of file loading. The Chart.yaml file needs to come first for +// later comparison checks. See https://github.com/helm/helm/pull/8948 +func TestLoadFilesOrder(t *testing.T) { + modTime := time.Now() + goodFiles := []*archive.BufferedFile{ + { + Name: "requirements.yaml", + ModTime: modTime, + Data: []byte("dependencies:"), + }, + { + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), + }, + + { + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), + }, + { + Name: "Chart.yaml", + ModTime: modTime, + Data: []byte(`apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +`), + }, + } + + // Capture stderr to make sure message about Chart.yaml handle dependencies + // is not present + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Unable to create pipe: %s", err) + } + stderr := log.Writer() + log.SetOutput(w) + defer func() { + log.SetOutput(stderr) + }() + + _, err = LoadFiles(context.Background(), goodFiles) + if err != nil { + t.Errorf("Expected good files to be loaded, got %v", err) + } + w.Close() + + var text bytes.Buffer + io.Copy(&text, r) + if text.String() != "" { + t.Errorf("Expected no message to Stderr, got %s", text.String()) + } + +} + +// Packaging the chart on a Windows machine will produce an +// archive that has \\ as delimiters. Test that we support these archives +func TestLoadFileBackslash(t *testing.T) { + c, err := Load(context.Background(), "testdata/frobnitz_backslash-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyChartFileAndTemplate(t, c, "frobnitz_backslash") + verifyChart(t, c) + verifyDependencies(t, c) +} + +func TestLoadV3WithReqs(t *testing.T) { + l, err := Loader("testdata/frobnitz.v3.reqs") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadInvalidArchive(t *testing.T) { + tmpdir := t.TempDir() + + writeTar := func(filename, internalPath string, body []byte) { + dest, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + zipper := gzip.NewWriter(dest) + tw := tar.NewWriter(zipper) + + h := &tar.Header{ + Name: internalPath, + Mode: 0755, + Size: int64(len(body)), + ModTime: time.Now(), + } + if err := tw.WriteHeader(h); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(body); err != nil { + t.Fatal(err) + } + tw.Close() + zipper.Close() + dest.Close() + } + + for _, tt := range []struct { + chartname string + internal string + expectError string + }{ + {"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"}, + {"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"}, + {"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"}, + {"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"}, + {"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"}, + {"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"}, + {"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"}, + {"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"}, + + // Under special circumstances, this can get normalized to things that look like absolute Windows paths + {"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"}, + {"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"}, + {"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"}, + } { + illegalChart := filepath.Join(tmpdir, tt.chartname) + writeTar(illegalChart, tt.internal, []byte("hello: world")) + _, err := Load(context.Background(), illegalChart) + if err == nil { + t.Fatal("expected error when unpacking illegal files") + } + if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname) + } + } + + // Make sure that absolute path gets interpreted as relative + illegalChart := filepath.Join(tmpdir, "abs-path.tgz") + writeTar(illegalChart, "/Chart.yaml", []byte("hello: world")) + _, err := Load(context.Background(), illegalChart) + if err.Error() != "validation: chart.metadata.name is required" { + t.Error(err) + } + + // And just to validate that the above was not spurious + illegalChart = filepath.Join(tmpdir, "abs-path2.tgz") + writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world")) + _, err = Load(context.Background(), illegalChart) + if err.Error() != "Chart.yaml file is missing" { + t.Errorf("Unexpected error message: %s", err) + } + + // Finally, test that drive letter gets stripped off on Windows + illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz") + writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world")) + _, err = Load(context.Background(), illegalChart) + if err.Error() != "validation: chart.metadata.name is required" { + t.Error(err) + } +} + +func TestLoadValues(t *testing.T) { + testCases := map[string]struct { + data []byte + expctedValues map[string]interface{} + }{ + "It should load values correctly": { + data: []byte(` +foo: + image: foo:v1 +bar: + version: v2 +`), + expctedValues: map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "foo:v1", + }, + "bar": map[string]interface{}{ + "version": "v2", + }, + }, + }, + "It should load values correctly with multiple documents in one file": { + data: []byte(` +foo: + image: foo:v1 +bar: + version: v2 +--- +foo: + image: foo:v2 +`), + expctedValues: map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "foo:v2", + }, + "bar": map[string]interface{}{ + "version": "v2", + }, + }, + }, + } + for testName, testCase := range testCases { + t.Run(testName, func(tt *testing.T) { + values, err := LoadValues(bytes.NewReader(testCase.data)) + if err != nil { + tt.Fatal(err) + } + if !reflect.DeepEqual(values, testCase.expctedValues) { + tt.Errorf("Expected values: %v, got %v", testCase.expctedValues, values) + } + }) + } +} + +func TestMergeValuesV3(t *testing.T) { + nestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "stuff", + }, + } + anotherNestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + flatMap := map[string]interface{}{ + "foo": "bar", + "baz": "stuff", + } + anotherFlatMap := map[string]interface{}{ + "testing": "fun", + } + + testMap := MergeMaps(flatMap, nestedMap) + equal := reflect.DeepEqual(testMap, nestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) + } + + testMap = MergeMaps(nestedMap, flatMap) + equal = reflect.DeepEqual(testMap, flatMap) + if !equal { + t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) + } + + testMap = MergeMaps(nestedMap, anotherNestedMap) + equal = reflect.DeepEqual(testMap, anotherNestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) + } + + testMap = MergeMaps(anotherFlatMap, anotherNestedMap) + expectedMap := map[string]interface{}{ + "testing": "fun", + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + equal = reflect.DeepEqual(testMap, expectedMap) + if !equal { + t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) + } +} + +func verifyChart(t *testing.T, c *chart.Chart) { + t.Helper() + if c.Name() == "" { + t.Fatalf("No chart metadata found on %v", c) + } + t.Logf("Verifying chart %s", c.Name()) + if len(c.Templates) != 1 { + t.Errorf("Expected 1 template, got %d", len(c.Templates)) + } + + numfiles := 6 + if len(c.Files) != numfiles { + t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) + for _, n := range c.Files { + t.Logf("\t%s", n.Name) + } + } + + if len(c.Dependencies()) != 2 { + t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies()) + for _, d := range c.Dependencies() { + t.Logf("\tSubchart: %s\n", d.Name()) + } + } + + expect := map[string]map[string]string{ + "alpine": { + "version": "0.1.0", + }, + "mariner": { + "version": "4.3.2", + }, + } + + for _, dep := range c.Dependencies() { + if dep.Metadata == nil { + t.Fatalf("expected metadata on dependency: %v", dep) + } + exp, ok := expect[dep.Name()] + if !ok { + t.Fatalf("Unknown dependency %s", dep.Name()) + } + if exp["version"] != dep.Metadata.Version { + t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) + } + } + +} + +func verifyDependencies(t *testing.T, c *chart.Chart) { + t.Helper() + if len(c.Metadata.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) + } + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := c.Metadata.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + +func verifyDependenciesLock(t *testing.T, c *chart.Chart) { + t.Helper() + if len(c.Metadata.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) + } + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := c.Metadata.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + +func verifyFrobnitz(t *testing.T, c *chart.Chart) { + t.Helper() + verifyChartFileAndTemplate(t, c, "frobnitz") +} + +func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { + t.Helper() + if c.Metadata == nil { + t.Fatal("Metadata is nil") + } + if c.Name() != name { + t.Errorf("Expected %s, got %s", name, c.Name()) + } + if len(c.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(c.Templates)) + } + if c.Templates[0].Name != "templates/template.tpl" { + t.Errorf("Unexpected template: %s", c.Templates[0].Name) + } + if len(c.Templates[0].Data) == 0 { + t.Error("No template data.") + } + if len(c.Files) != 6 { + t.Fatalf("Expected 6 Files, got %d", len(c.Files)) + } + if len(c.Dependencies()) != 2 { + t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies())) + } + if len(c.Metadata.Dependencies) != 2 { + t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies)) + } + if len(c.Lock.Dependencies) != 2 { + t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies)) + } + + for _, dep := range c.Dependencies() { + switch dep.Name() { + case "mariner": + case "alpine": + if len(dep.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(dep.Templates)) + } + if dep.Templates[0].Name != "templates/alpine-pod.yaml" { + t.Errorf("Unexpected template: %s", dep.Templates[0].Name) + } + if len(dep.Templates[0].Data) == 0 { + t.Error("No template data.") + } + if len(dep.Files) != 1 { + t.Fatalf("Expected 1 Files, got %d", len(dep.Files)) + } + if len(dep.Dependencies()) != 2 { + t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies())) + } + default: + t.Errorf("Unexpected dependency %s", dep.Name()) + } + } +} + +func verifyBomStripped(t *testing.T, files []*common.File) { + t.Helper() + for _, file := range files { + if bytes.HasPrefix(file.Data, utf8bom) { + t.Errorf("Byte Order Mark still present in processed file %s", file.Name) + } + } +} diff --git a/pkg/helm/pkg/chart/loader/testdata/LICENSE b/pkg/helm/intern/chart/v3/loader/testdata/LICENSE similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/LICENSE rename to pkg/helm/intern/chart/v3/loader/testdata/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/albatross/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/albatross/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/albatross/Chart.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/albatross/Chart.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/albatross/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/albatross/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/albatross/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/albatross/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz-1.2.3.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz-1.2.3.tgz new file mode 100644 index 00000000..de28e412 Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz-1.2.3.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/.helmignore b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/.helmignore rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml new file mode 100644 index 00000000..1b63fc3e --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/INSTALL.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/INSTALL.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/LICENSE b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/LICENSE rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/_ignore_me b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/_ignore_me rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/docs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/docs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/icon.svg b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/icon.svg rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/ignore/me.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/ignore/me.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/templates/template.tpl b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/templates/template.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/.helmignore b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/.helmignore similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/.helmignore rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/.helmignore diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/Chart.lock b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/Chart.lock similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/Chart.lock rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/Chart.lock diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/Chart.yaml new file mode 100644 index 00000000..1b63fc3e --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/INSTALL.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/INSTALL.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/LICENSE b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/LICENSE similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/LICENSE rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..5c6bc4dc Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/docs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/docs/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/docs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/icon.svg b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/icon.svg similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/icon.svg rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/ignore/me.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/ignore/me.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/templates/template.tpl b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/templates/template.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz new file mode 100644 index 00000000..dfbe88a7 Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/.helmignore b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/.helmignore old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/.helmignore rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/.helmignore diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/Chart.lock b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/Chart.lock rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml new file mode 100755 index 00000000..6a952e33 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz_backslash +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/INSTALL.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/INSTALL.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/LICENSE b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/LICENSE old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/LICENSE rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/README.md old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/_ignore_me b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/_ignore_me rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml new file mode 100755 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml new file mode 100755 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz new file mode 100755 index 00000000..5c6bc4dc Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/docs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/docs/README.md old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/docs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/icon.svg b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/icon.svg old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/icon.svg rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/ignore/me.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/ignore/me.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/templates/template.tpl b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/templates/template.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/values.yaml old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_backslash/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom.tgz new file mode 100644 index 00000000..7f0edc6b Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/.helmignore rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.lock rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml new file mode 100644 index 00000000..924fae6f --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/INSTALL.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/LICENSE rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/_ignore_me rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml new file mode 100644 index 00000000..6fe4f411 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..0732c7d7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..5c6bc4dc Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/docs/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/docs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/icon.svg b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/icon.svg old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/icon.svg rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/ignore/me.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/ignore/me.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/templates/template.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/.helmignore b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/.helmignore rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/Chart.lock b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/Chart.lock rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml new file mode 100644 index 00000000..1b63fc3e --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/INSTALL.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/INSTALL.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/LICENSE b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/LICENSE rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/_ignore_me b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/_ignore_me rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..5c6bc4dc Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/docs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/docs/README.md old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/docs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/icon.svg similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/icon.svg rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/ignore/me.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/ignore/me.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/null b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/null similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/null rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/null diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/templates/template.tpl b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/templates/template.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/.helmignore b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/.helmignore rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.lock b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.lock rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml new file mode 100644 index 00000000..1b63fc3e --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/INSTALL.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/INSTALL.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..5c6bc4dc Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/docs/README.md b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/docs/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/docs/README.md rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/icon.svg b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/icon.svg similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/icon.svg rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/ignore/me.txt b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/ignore/me.txt rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/templates/template.tpl b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/templates/template.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml diff --git a/pkg/helm/intern/chart/v3/loader/testdata/genfrob.sh b/pkg/helm/intern/chart/v3/loader/testdata/genfrob.sh new file mode 100755 index 00000000..eae68906 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/genfrob.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +# Pack the albatross chart into the mariner chart. +echo "Packing albatross into mariner" +tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross + +echo "Packing mariner into frobnitz" +tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner +cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_backslash/charts/ +cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_with_bom/charts/ +cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_with_dev_null/charts/ +cp frobnitz/charts/mariner-4.3.2.tgz frobnitz_with_symlink/charts/ + +# Pack the frobnitz chart. +echo "Packing frobnitz" +tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash +tar --exclude=ignore/* -zcvf frobnitz_with_bom.tgz frobnitz_with_bom diff --git a/pkg/helm/intern/chart/v3/loader/testdata/mariner/Chart.yaml b/pkg/helm/intern/chart/v3/loader/testdata/mariner/Chart.yaml new file mode 100644 index 00000000..4d3eea73 --- /dev/null +++ b/pkg/helm/intern/chart/v3/loader/testdata/mariner/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v3 +name: mariner +description: A Helm chart for Kubernetes +version: 4.3.2 +home: "" +dependencies: + - name: albatross + repository: https://example.com/mariner/charts + version: "0.1.0" diff --git a/pkg/helm/intern/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/helm/intern/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz new file mode 100644 index 00000000..ec7bfbfc Binary files /dev/null and b/pkg/helm/intern/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/loader/testdata/mariner/templates/placeholder.tpl b/pkg/helm/intern/chart/v3/loader/testdata/mariner/templates/placeholder.tpl similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/mariner/templates/placeholder.tpl rename to pkg/helm/intern/chart/v3/loader/testdata/mariner/templates/placeholder.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/mariner/values.yaml b/pkg/helm/intern/chart/v3/loader/testdata/mariner/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/mariner/values.yaml rename to pkg/helm/intern/chart/v3/loader/testdata/mariner/values.yaml diff --git a/pkg/helm/intern/chart/v3/metadata.go b/pkg/helm/intern/chart/v3/metadata.go new file mode 100644 index 00000000..4629d571 --- /dev/null +++ b/pkg/helm/intern/chart/v3/metadata.go @@ -0,0 +1,178 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v3 + +import ( + "path/filepath" + "strings" + "unicode" + + "github.com/Masterminds/semver/v3" +) + +// Maintainer describes a Chart maintainer. +type Maintainer struct { + // Name is a user name or organization name + Name string `json:"name,omitempty"` + // Email is an optional email address to contact the named maintainer + Email string `json:"email,omitempty"` + // URL is an optional URL to an address for the named maintainer + URL string `json:"url,omitempty"` +} + +// Validate checks valid data and sanitizes string characters. +func (m *Maintainer) Validate() error { + if m == nil { + return ValidationError("maintainers must not contain empty or null nodes") + } + m.Name = sanitizeString(m.Name) + m.Email = sanitizeString(m.Email) + m.URL = sanitizeString(m.URL) + return nil +} + +// Metadata for a Chart file. This models the structure of a Chart.yaml file. +type Metadata struct { + // The name of the chart. Required. + Name string `json:"name,omitempty"` + // The URL to a relevant project page, git repo, or contact person + Home string `json:"home,omitempty"` + // Source is the URL to the source code of this chart + Sources []string `json:"sources,omitempty"` + // A SemVer 2 conformant version string of the chart. Required. + Version string `json:"version,omitempty"` + // A one-sentence description of the chart + Description string `json:"description,omitempty"` + // A list of string keywords + Keywords []string `json:"keywords,omitempty"` + // A list of name and URL/email address combinations for the maintainer(s) + Maintainers []*Maintainer `json:"maintainers,omitempty"` + // The URL to an icon file. + Icon string `json:"icon,omitempty"` + // The API Version of this chart. Required. + APIVersion string `json:"apiVersion,omitempty"` + // The condition to check to enable chart + Condition string `json:"condition,omitempty"` + // The tags to check to enable chart + Tags string `json:"tags,omitempty"` + // The version of the application enclosed inside of this chart. + AppVersion string `json:"appVersion,omitempty"` + // Whether or not this chart is deprecated + Deprecated bool `json:"deprecated,omitempty"` + // Annotations are additional mappings uninterpreted by Helm, + // made available for inspection by other applications. + Annotations map[string]string `json:"annotations,omitempty"` + // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. + KubeVersion string `json:"kubeVersion,omitempty"` + // Dependencies are a list of dependencies for a chart. + Dependencies []*Dependency `json:"dependencies,omitempty"` + // Specifies the chart type: application or library + Type string `json:"type,omitempty"` +} + +// Validate checks the metadata for known issues and sanitizes string +// characters. +func (md *Metadata) Validate() error { + if md == nil { + return ValidationError("chart.metadata is required") + } + + md.Name = sanitizeString(md.Name) + md.Description = sanitizeString(md.Description) + md.Home = sanitizeString(md.Home) + md.Icon = sanitizeString(md.Icon) + md.Condition = sanitizeString(md.Condition) + md.Tags = sanitizeString(md.Tags) + md.AppVersion = sanitizeString(md.AppVersion) + md.KubeVersion = sanitizeString(md.KubeVersion) + for i := range md.Sources { + md.Sources[i] = sanitizeString(md.Sources[i]) + } + for i := range md.Keywords { + md.Keywords[i] = sanitizeString(md.Keywords[i]) + } + + if md.APIVersion == "" { + return ValidationError("chart.metadata.apiVersion is required") + } + if md.Name == "" { + return ValidationError("chart.metadata.name is required") + } + + if md.Name != filepath.Base(md.Name) { + return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) + } + + if md.Version == "" { + return ValidationError("chart.metadata.version is required") + } + if !isValidSemver(md.Version) { + return ValidationErrorf("chart.metadata.version %q is invalid", md.Version) + } + if !isValidChartType(md.Type) { + return ValidationError("chart.metadata.type must be application or library") + } + + for _, m := range md.Maintainers { + if err := m.Validate(); err != nil { + return err + } + } + + // Aliases need to be validated here to make sure that the alias name does + // not contain any illegal characters. + dependencies := map[string]*Dependency{} + for _, dependency := range md.Dependencies { + if err := dependency.Validate(); err != nil { + return err + } + key := dependency.Name + if dependency.Alias != "" { + key = dependency.Alias + } + if dependencies[key] != nil { + return ValidationErrorf("more than one dependency with name or alias %q", key) + } + dependencies[key] = dependency + } + return nil +} + +func isValidChartType(in string) bool { + switch in { + case "", "application", "library": + return true + } + return false +} + +func isValidSemver(v string) bool { + _, err := semver.NewVersion(v) + return err == nil +} + +// sanitizeString normalize spaces and removes non-printable characters. +func sanitizeString(str string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return ' ' + } + if unicode.IsPrint(r) { + return r + } + return -1 + }, str) +} diff --git a/pkg/helm/intern/chart/v3/metadata_test.go b/pkg/helm/intern/chart/v3/metadata_test.go new file mode 100644 index 00000000..596a0369 --- /dev/null +++ b/pkg/helm/intern/chart/v3/metadata_test.go @@ -0,0 +1,201 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v3 + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + md *Metadata + err error + }{ + { + "chart without metadata", + nil, + ValidationError("chart.metadata is required"), + }, + { + "chart without apiVersion", + &Metadata{Name: "test", Version: "1.0"}, + ValidationError("chart.metadata.apiVersion is required"), + }, + { + "chart without name", + &Metadata{APIVersion: "v3", Version: "1.0"}, + ValidationError("chart.metadata.name is required"), + }, + { + "chart without name", + &Metadata{Name: "../../test", APIVersion: "v3", Version: "1.0"}, + ValidationError("chart.metadata.name \"../../test\" is invalid"), + }, + { + "chart without version", + &Metadata{Name: "test", APIVersion: "v3"}, + ValidationError("chart.metadata.version is required"), + }, + { + "chart with bad type", + &Metadata{Name: "test", APIVersion: "v3", Version: "1.0", Type: "test"}, + ValidationError("chart.metadata.type must be application or library"), + }, + { + "chart without dependency", + &Metadata{Name: "test", APIVersion: "v3", Version: "1.0", Type: "application"}, + nil, + }, + { + "dependency with valid alias", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "dependency", Alias: "legal-alias"}, + }, + }, + nil, + }, + { + "dependency with bad characters in alias", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "bad", Alias: "illegal alias"}, + }, + }, + ValidationError("dependency \"bad\" has disallowed characters in the alias"), + }, + { + "same dependency twice", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Alias: ""}, + {Name: "foo", Alias: ""}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + "two dependencies with alias from second dependency shadowing first one", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Alias: ""}, + {Name: "bar", Alias: "foo"}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + // this case would make sense and could work in future versions of Helm, currently template rendering would + // result in undefined behaviour + "same dependency twice with different version", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Alias: "", Version: "1.2.3"}, + {Name: "foo", Alias: "", Version: "1.0.0"}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + // this case would make sense and could work in future versions of Helm, currently template rendering would + // result in undefined behaviour + "two dependencies with same name but different repos", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Repository: "repo-0"}, + {Name: "foo", Repository: "repo-1"}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + "dependencies has nil", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + nil, + }, + }, + ValidationError("dependencies must not contain empty or null nodes"), + }, + { + "maintainer not empty", + &Metadata{ + Name: "test", + APIVersion: "v3", + Version: "1.0", + Type: "application", + Maintainers: []*Maintainer{ + nil, + }, + }, + ValidationError("maintainers must not contain empty or null nodes"), + }, + { + "version invalid", + &Metadata{APIVersion: "3", Name: "test", Version: "1.2.3.4"}, + ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"), + }, + } + + for _, tt := range tests { + result := tt.md.Validate() + if result != tt.err { + t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name) + } + } +} + +func TestValidate_sanitize(t *testing.T) { + md := &Metadata{APIVersion: "3", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}} + if err := md.Validate(); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if md.Description != "description test" { + t.Fatalf("description was not sanitized: %q", md.Description) + } + if md.Maintainers[0].Name != " " { + t.Fatal("maintainer name was not sanitized") + } +} diff --git a/pkg/helm/intern/chart/v3/util/chartfile.go b/pkg/helm/intern/chart/v3/util/chartfile.go new file mode 100644 index 00000000..a535d4b8 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/chartfile.go @@ -0,0 +1,96 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" +) + +// LoadChartfile loads a Chart.yaml file into a *chart.Metadata. +func LoadChartfile(filename string) (*chart.Metadata, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := new(chart.Metadata) + err = yaml.Unmarshal(b, y) + return y, err +} + +// StrictLoadChartfile loads a Chart.yaml into a *chart.Metadata using a strict unmarshaling +func StrictLoadChartfile(filename string) (*chart.Metadata, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := new(chart.Metadata) + err = yaml.UnmarshalStrict(b, y) + return y, err +} + +// SaveChartfile saves the given metadata as a Chart.yaml file at the given path. +// +// 'filename' should be the complete path and filename ('foo/Chart.yaml') +func SaveChartfile(filename string, cf *chart.Metadata) error { + out, err := yaml.Marshal(cf) + if err != nil { + return err + } + return os.WriteFile(filename, out, 0644) +} + +// IsChartDir validate a chart directory. +// +// Checks for a valid Chart.yaml. +func IsChartDir(dirName string) (bool, error) { + if fi, err := os.Stat(dirName); err != nil { + return false, err + } else if !fi.IsDir() { + return false, fmt.Errorf("%q is not a directory", dirName) + } + + chartYaml := filepath.Join(dirName, ChartfileName) + if _, err := os.Stat(chartYaml); errors.Is(err, fs.ErrNotExist) { + return false, fmt.Errorf("no %s exists in directory %q", ChartfileName, dirName) + } + + chartYamlContent, err := os.ReadFile(chartYaml) + if err != nil { + return false, fmt.Errorf("cannot read %s in directory %q", ChartfileName, dirName) + } + + chartContent := new(chart.Metadata) + if err := yaml.Unmarshal(chartYamlContent, &chartContent); err != nil { + return false, err + } + if chartContent == nil { + return false, fmt.Errorf("chart metadata (%s) missing", ChartfileName) + } + if chartContent.Name == "" { + return false, fmt.Errorf("invalid chart (%s): name must not be empty", ChartfileName) + } + + return true, nil +} diff --git a/pkg/helm/intern/chart/v3/util/chartfile_test.go b/pkg/helm/intern/chart/v3/util/chartfile_test.go new file mode 100644 index 00000000..8d721a55 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/chartfile_test.go @@ -0,0 +1,117 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" +) + +const testfile = "testdata/chartfiletest.yaml" + +func TestLoadChartfile(t *testing.T) { + f, err := LoadChartfile(testfile) + if err != nil { + t.Errorf("Failed to open %s: %s", testfile, err) + return + } + verifyChartfile(t, f, "frobnitz") +} + +func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { + t.Helper() + if f == nil { //nolint:staticcheck + t.Fatal("Failed verifyChartfile because f is nil") + } + + if f.Name != name { + t.Errorf("Expected %s, got %s", name, f.Name) + } + + if f.Description != "This is a frobnitz." { + t.Errorf("Unexpected description %q", f.Description) + } + + if f.Version != "1.2.3" { + t.Errorf("Unexpected version %q", f.Version) + } + + if len(f.Maintainers) != 2 { + t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) + } + + if f.Maintainers[0].Name != "The Helm Team" { + t.Errorf("Unexpected maintainer name.") + } + + if f.Maintainers[1].Email != "nobody@example.com" { + t.Errorf("Unexpected maintainer email.") + } + + if len(f.Sources) != 1 { + t.Fatalf("Unexpected number of sources") + } + + if f.Sources[0] != "https://example.com/foo/bar" { + t.Errorf("Expected https://example.com/foo/bar, got %s", f.Sources) + } + + if f.Home != "http://example.com" { + t.Error("Unexpected home.") + } + + if f.Icon != "https://example.com/64x64.png" { + t.Errorf("Unexpected icon: %q", f.Icon) + } + + if len(f.Keywords) != 3 { + t.Error("Unexpected keywords") + } + + if len(f.Annotations) != 2 { + t.Fatalf("Unexpected annotations") + } + + if want, got := "extravalue", f.Annotations["extrakey"]; want != got { + t.Errorf("Want %q, but got %q", want, got) + } + + if want, got := "anothervalue", f.Annotations["anotherkey"]; want != got { + t.Errorf("Want %q, but got %q", want, got) + } + + kk := []string{"frobnitz", "sprocket", "dodad"} + for i, k := range f.Keywords { + if kk[i] != k { + t.Errorf("Expected %q, got %q", kk[i], k) + } + } +} + +func TestIsChartDir(t *testing.T) { + validChartDir, err := IsChartDir("testdata/frobnitz") + if !validChartDir { + t.Errorf("unexpected error while reading chart-directory: (%v)", err) + return + } + validChartDir, err = IsChartDir("testdata") + if validChartDir || err == nil { + t.Errorf("expected error but did not get any") + return + } +} diff --git a/pkg/helm/intern/chart/v3/util/compatible.go b/pkg/helm/intern/chart/v3/util/compatible.go new file mode 100644 index 00000000..d384d2d4 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/compatible.go @@ -0,0 +1,34 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import "github.com/Masterminds/semver/v3" + +// IsCompatibleRange compares a version to a constraint. +// It returns true if the version matches the constraint, and false in all other cases. +func IsCompatibleRange(constraint, ver string) bool { + sv, err := semver.NewVersion(ver) + if err != nil { + return false + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false + } + return c.Check(sv) +} diff --git a/pkg/helm/intern/chart/v3/util/compatible_test.go b/pkg/helm/intern/chart/v3/util/compatible_test.go new file mode 100644 index 00000000..e17d33e3 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/compatible_test.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package version represents the current version of the project. +package util + +import "testing" + +func TestIsCompatibleRange(t *testing.T) { + tests := []struct { + constraint string + ver string + expected bool + }{ + {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, + {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, + {"v2.0.0", "v2.0.0-alpha.4", false}, + {"v2.0.0-alpha.4", "v2.0.0", false}, + {"~v2.0.0", "v2.0.1", true}, + {"v2", "v2.0.0", true}, + {">2.0.0", "v2.1.1", true}, + {"v2.1.*", "v2.1.1", true}, + } + + for _, tt := range tests { + if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected { + t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver) + } + } +} diff --git a/pkg/helm/intern/chart/v3/util/create.go b/pkg/helm/intern/chart/v3/util/create.go new file mode 100644 index 00000000..6051ba33 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/create.go @@ -0,0 +1,835 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +// chartName is a regular expression for testing the supplied name of a chart. +// This regular expression is probably stricter than it needs to be. We can relax it +// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be +// problematic. +var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$") + +const ( + // ChartfileName is the default Chart file name. + ChartfileName = "Chart.yaml" + // ValuesfileName is the default values file name. + ValuesfileName = "values.yaml" + // SchemafileName is the default values schema file name. + SchemafileName = "values.schema.json" + // TemplatesDir is the relative directory name for templates. + TemplatesDir = "templates" + // ChartsDir is the relative directory name for charts dependencies. + ChartsDir = "charts" + // TemplatesTestsDir is the relative directory name for tests. + TemplatesTestsDir = TemplatesDir + sep + "tests" + // IgnorefileName is the name of the Helm ignore file. + IgnorefileName = ".helmignore" + // IngressFileName is the name of the example ingress file. + IngressFileName = TemplatesDir + sep + "ingress.yaml" + // HTTPRouteFileName is the name of the example HTTPRoute file. + HTTPRouteFileName = TemplatesDir + sep + "httproute.yaml" + // DeploymentName is the name of the example deployment file. + DeploymentName = TemplatesDir + sep + "deployment.yaml" + // ServiceName is the name of the example service file. + ServiceName = TemplatesDir + sep + "service.yaml" + // ServiceAccountName is the name of the example serviceaccount file. + ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml" + // HorizontalPodAutoscalerName is the name of the example hpa file. + HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml" + // NotesName is the name of the example NOTES.txt file. + NotesName = TemplatesDir + sep + "NOTES.txt" + // HelpersName is the name of the example helpers file. + HelpersName = TemplatesDir + sep + "_helpers.tpl" + // TestConnectionName is the name of the example test file. + TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml" +) + +// maxChartNameLength is lower than the limits we know of with certain file systems, +// and with certain Kubernetes fields. +const maxChartNameLength = 250 + +const sep = string(filepath.Separator) + +const defaultChartfile = `apiVersion: v3 +name: %s +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +` + +const defaultValues = `# Default values for %s. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # For publicly distributed charts, we recommend leaving 'resources' commented out. + # This makes resource allocation a conscious choice for the user and increases the chances + # charts run on a wide range of environments from low-resource clusters like Minikube to those + # with strict resource policies. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} +` + +const defaultIgnore = `# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +` + +const defaultIngress = `{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include ".fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} +` + +const defaultHTTPRoute = `{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include ".fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} +` + +const defaultDeployment = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include ".selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include ".labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include ".serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +` + +const defaultService = `apiVersion: v1 +kind: Service +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include ".selectorLabels" . | nindent 4 }} +` + +const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include ".serviceAccountName" . }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +` + +const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include ".fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +` + +const defaultNotes = `1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include ".fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include ".fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include ".fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include ".name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +` + +const defaultHelpers = `{{/* +Expand the name of the chart. +*/}} +{{- define ".name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define ".fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define ".chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define ".labels" -}} +helm.sh/chart: {{ include ".chart" . }} +{{ include ".selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define ".selectorLabels" -}} +app.kubernetes.io/name: {{ include ".name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define ".serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include ".fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} +` + +const defaultTestConnection = `apiVersion: v1 +kind: Pod +metadata: + name: "{{ include ".fullname" . }}-test-connection" + labels: + {{- include ".labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include ".fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never +` + +// Stderr is an io.Writer to which error messages can be written +// +// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward +// compatibility. +var Stderr io.Writer = os.Stderr + +// CreateFrom creates a new chart, but scaffolds it from the src chart. +func CreateFrom(chartfile *chart.Metadata, dest, src string) error { + schart, err := loader.Load(context.Background(), src) + if err != nil { + return fmt.Errorf("could not load %s: %w", src, err) + } + + schart.Metadata = chartfile + + var updatedTemplates []*common.File + + for _, template := range schart.Templates { + newData := transform(string(template.Data), schart.Name()) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData}) + } + + schart.Templates = updatedTemplates + b, err := yaml.Marshal(schart.Values) + if err != nil { + return fmt.Errorf("reading values file: %w", err) + } + + var m map[string]interface{} + if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { + return fmt.Errorf("transforming values file: %w", err) + } + schart.Values = m + + // SaveDir looks for the file values.yaml when saving rather than the values + // key in order to preserve the comments in the YAML. The name placeholder + // needs to be replaced on that file. + for _, f := range schart.Raw { + if f.Name == ValuesfileName { + f.Data = transform(string(f.Data), schart.Name()) + } + } + + return SaveDir(schart, dest) +} + +// Create creates a new chart in a directory. +// +// Inside of dir, this will create a directory based on the name of +// chartfile.Name. It will then write the Chart.yaml into this directory and +// create the (empty) appropriate directories. +// +// The returned string will point to the newly created directory. It will be +// an absolute path, even if the provided base directory was relative. +// +// If dir does not exist, this will return an error. +// If Chart.yaml or any directories cannot be created, this will return an +// error. In such a case, this will attempt to clean up by removing the +// new chart directory. +func Create(name, dir string) (string, error) { + + // Sanity-check the name of a chart so user doesn't create one that causes problems. + if err := validateChartName(name); err != nil { + return "", err + } + + path, err := filepath.Abs(dir) + if err != nil { + return path, err + } + + if fi, err := os.Stat(path); err != nil { + return path, err + } else if !fi.IsDir() { + return path, fmt.Errorf("no such directory %s", path) + } + + cdir := filepath.Join(path, name) + if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { + return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir) + } + + // Note: If adding a new template below (i.e., to `helm create`) which is disabled by default (similar to hpa and + // ingress below); or making an existing template disabled by default, add the enabling condition in + // `TestHelmCreateChart_CheckDeprecatedWarnings` in `pkg/lint/lint_test.go` to make it run through deprecation checks + // with latest Kubernetes version. + files := []struct { + path string + content []byte + }{ + { + // Chart.yaml + path: filepath.Join(cdir, ChartfileName), + content: fmt.Appendf(nil, defaultChartfile, name), + }, + { + // values.yaml + path: filepath.Join(cdir, ValuesfileName), + content: fmt.Appendf(nil, defaultValues, name), + }, + { + // .helmignore + path: filepath.Join(cdir, IgnorefileName), + content: []byte(defaultIgnore), + }, + { + // ingress.yaml + path: filepath.Join(cdir, IngressFileName), + content: transform(defaultIngress, name), + }, + { + // httproute.yaml + path: filepath.Join(cdir, HTTPRouteFileName), + content: transform(defaultHTTPRoute, name), + }, + { + // deployment.yaml + path: filepath.Join(cdir, DeploymentName), + content: transform(defaultDeployment, name), + }, + { + // service.yaml + path: filepath.Join(cdir, ServiceName), + content: transform(defaultService, name), + }, + { + // serviceaccount.yaml + path: filepath.Join(cdir, ServiceAccountName), + content: transform(defaultServiceAccount, name), + }, + { + // hpa.yaml + path: filepath.Join(cdir, HorizontalPodAutoscalerName), + content: transform(defaultHorizontalPodAutoscaler, name), + }, + { + // NOTES.txt + path: filepath.Join(cdir, NotesName), + content: transform(defaultNotes, name), + }, + { + // _helpers.tpl + path: filepath.Join(cdir, HelpersName), + content: transform(defaultHelpers, name), + }, + { + // test-connection.yaml + path: filepath.Join(cdir, TestConnectionName), + content: transform(defaultTestConnection, name), + }, + } + + for _, file := range files { + if _, err := os.Stat(file.path); err == nil { + // There is no handle to a preferred output stream here. + fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path) + } + if err := writeFile(file.path, file.content); err != nil { + return cdir, err + } + } + // Need to add the ChartsDir explicitly as it does not contain any file OOTB + if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil { + return cdir, err + } + return cdir, nil +} + +// transform performs a string replacement of the specified source for +// a given key with the replacement string +func transform(src, replacement string) []byte { + return []byte(strings.ReplaceAll(src, "", replacement)) +} + +func writeFile(name string, content []byte) error { + if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { + return err + } + return os.WriteFile(name, content, 0644) +} + +func validateChartName(name string) error { + if name == "" || len(name) > maxChartNameLength { + return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) + } + if !chartName.MatchString(name) { + return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) + } + return nil +} diff --git a/pkg/helm/intern/chart/v3/util/create_test.go b/pkg/helm/intern/chart/v3/util/create_test.go new file mode 100644 index 00000000..3702d2d3 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/create_test.go @@ -0,0 +1,173 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "bytes" + "context" + "os" + "path/filepath" + "testing" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" +) + +func TestCreate(t *testing.T) { + tdir := t.TempDir() + + c, err := Create("foo", tdir) + if err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + mychart, err := loader.LoadDir(context.Background(), c) + if err != nil { + t.Fatalf("Failed to load newly created chart %q: %s", c, err) + } + + if mychart.Name() != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) + } + + for _, f := range []string{ + ChartfileName, + DeploymentName, + HelpersName, + IgnorefileName, + NotesName, + ServiceAccountName, + ServiceName, + TemplatesDir, + TemplatesTestsDir, + TestConnectionName, + ValuesfileName, + } { + if _, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } + } +} + +func TestCreateFrom(t *testing.T) { + tdir := t.TempDir() + + cf := &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "foo", + Version: "0.1.0", + } + srcdir := "./testdata/frobnitz/charts/mariner" + + if err := CreateFrom(cf, tdir, srcdir); err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + c := filepath.Join(tdir, cf.Name) + mychart, err := loader.LoadDir(context.Background(), c) + if err != nil { + t.Fatalf("Failed to load newly created chart %q: %s", c, err) + } + + if mychart.Name() != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) + } + + for _, f := range []string{ + ChartfileName, + ValuesfileName, + filepath.Join(TemplatesDir, "placeholder.tpl"), + } { + if _, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } + + // Check each file to make sure has been replaced + b, err := os.ReadFile(filepath.Join(dir, f)) + if err != nil { + t.Errorf("Unable to read file %s: %s", f, err) + } + if bytes.Contains(b, []byte("")) { + t.Errorf("File %s contains ", f) + } + } +} + +// TestCreate_Overwrite is a regression test for making sure that files are overwritten. +func TestCreate_Overwrite(t *testing.T) { + tdir := t.TempDir() + + var errlog bytes.Buffer + + if _, err := Create("foo", tdir); err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + tplname := filepath.Join(dir, "templates/hpa.yaml") + writeFile(tplname, []byte("FOO")) + + // Now re-run the create + Stderr = &errlog + if _, err := Create("foo", tdir); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(tplname) + if err != nil { + t.Fatal(err) + } + + if string(data) == "FOO" { + t.Fatal("File that should have been modified was not.") + } + + if errlog.Len() == 0 { + t.Errorf("Expected warnings about overwriting files.") + } +} + +func TestValidateChartName(t *testing.T) { + for name, shouldPass := range map[string]bool{ + "": false, + "abcdefghijklmnopqrstuvwxyz-_.": true, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true, + "$hello": false, + "Hellô": false, + "he%%o": false, + "he\nllo": false, + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false, + } { + if err := validateChartName(name); (err != nil) == shouldPass { + t.Errorf("test for %q failed", name) + } + } +} diff --git a/pkg/helm/intern/chart/v3/util/dependencies.go b/pkg/helm/intern/chart/v3/util/dependencies.go new file mode 100644 index 00000000..a9da2756 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/dependencies.go @@ -0,0 +1,381 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "log/slog" + "strings" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/copystructure" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" +) + +// ProcessDependencies checks through this chart's dependencies, processing accordingly. +func ProcessDependencies(c *chart.Chart, v common.Values) error { + if err := processDependencyEnabled(c, v, ""); err != nil { + return err + } + return processDependencyImportValues(c, true) +} + +// processDependencyConditions disables charts based on condition path value in values +func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { + if reqs == nil { + return + } + for _, r := range reqs { + for c := range strings.SplitSeq(strings.TrimSpace(r.Condition), ",") { + if len(c) > 0 { + // retrieve value + vv, err := cvals.PathValue(cpath + c) + if err == nil { + // if not bool, warn + if bv, ok := vv.(bool); ok { + r.Enabled = bv + break + } + slog.Warn("returned non-bool value", "path", c, "chart", r.Name) + } else if _, ok := err.(common.ErrNoValue); !ok { + // this is a real error + slog.Warn("the method PathValue returned error", slog.Any("error", err)) + } + } + } + } +} + +// processDependencyTags disables charts based on tags in values +func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { + if reqs == nil { + return + } + vt, err := cvals.Table("tags") + if err != nil { + return + } + for _, r := range reqs { + var hasTrue, hasFalse bool + for _, k := range r.Tags { + if b, ok := vt[k]; ok { + // if not bool, warn + if bv, ok := b.(bool); ok { + if bv { + hasTrue = true + } else { + hasFalse = true + } + } else { + slog.Warn("returned non-bool value", "tag", k, "chart", r.Name) + } + } + } + if !hasTrue && hasFalse { + r.Enabled = false + } else if hasTrue || !hasTrue && !hasFalse { + r.Enabled = true + } + } +} + +// getAliasDependency finds the chart for an alias dependency and copies parts that will be modified +func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Chart { + for _, c := range charts { + if c == nil { + continue + } + if c.Name() != dep.Name { + continue + } + if !IsCompatibleRange(dep.Version, c.Metadata.Version) { + continue + } + + out := *c + out.Metadata = copyMetadata(c.Metadata) + + // empty dependencies and shallow copy all dependencies, otherwise parent info may be corrupted if + // there is more than one dependency aliasing this chart + out.SetDependencies() + for _, dependency := range c.Dependencies() { + cpy := *dependency + out.AddDependency(&cpy) + } + + if dep.Alias != "" { + out.Metadata.Name = dep.Alias + } + return &out + } + return nil +} + +func copyMetadata(metadata *chart.Metadata) *chart.Metadata { + md := *metadata + + if md.Dependencies != nil { + dependencies := make([]*chart.Dependency, len(md.Dependencies)) + for i := range md.Dependencies { + dependency := *md.Dependencies[i] + dependencies[i] = &dependency + } + md.Dependencies = dependencies + } + return &md +} + +// processDependencyEnabled removes disabled charts from dependencies +func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error { + if c.Metadata.Dependencies == nil { + return nil + } + + var chartDependencies []*chart.Chart + // If any dependency is not a part of Chart.yaml + // then this should be added to chartDependencies. + // However, if the dependency is already specified in Chart.yaml + // we should not add it, as it would be processed from Chart.yaml anyway. + +Loop: + for _, existing := range c.Dependencies() { + for _, req := range c.Metadata.Dependencies { + if existing.Name() == req.Name && IsCompatibleRange(req.Version, existing.Metadata.Version) { + continue Loop + } + } + chartDependencies = append(chartDependencies, existing) + } + + for _, req := range c.Metadata.Dependencies { + if req == nil { + continue + } + if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil { + chartDependencies = append(chartDependencies, chartDependency) + } + if req.Alias != "" { + req.Name = req.Alias + } + } + c.SetDependencies(chartDependencies...) + + // set all to true + for _, lr := range c.Metadata.Dependencies { + lr.Enabled = true + } + cvals, err := util.CoalesceValues(c, v) + if err != nil { + return err + } + // flag dependencies as enabled/disabled + processDependencyTags(c.Metadata.Dependencies, cvals) + processDependencyConditions(c.Metadata.Dependencies, cvals, path) + // make a map of charts to remove + rm := map[string]struct{}{} + for _, r := range c.Metadata.Dependencies { + if !r.Enabled { + // remove disabled chart + rm[r.Name] = struct{}{} + } + } + // don't keep disabled charts in new slice + cd := []*chart.Chart{} + copy(cd, c.Dependencies()[:0]) + for _, n := range c.Dependencies() { + if _, ok := rm[n.Metadata.Name]; !ok { + cd = append(cd, n) + } + } + // don't keep disabled charts in metadata + cdMetadata := []*chart.Dependency{} + copy(cdMetadata, c.Metadata.Dependencies[:0]) + for _, n := range c.Metadata.Dependencies { + if _, ok := rm[n.Name]; !ok { + cdMetadata = append(cdMetadata, n) + } + } + + // recursively call self to process sub dependencies + for _, t := range cd { + subpath := path + t.Metadata.Name + "." + if err := processDependencyEnabled(t, cvals, subpath); err != nil { + return err + } + } + // set the correct dependencies in metadata + c.Metadata.Dependencies = nil + c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...) + c.SetDependencies(cd...) + + return nil +} + +// pathToMap creates a nested map given a YAML path in dot notation. +func pathToMap(path string, data map[string]interface{}) map[string]interface{} { + if path == "." { + return data + } + return set(parsePath(path), data) +} + +func parsePath(key string) []string { return strings.Split(key, ".") } + +func set(path []string, data map[string]interface{}) map[string]interface{} { + if len(path) == 0 { + return nil + } + cur := data + for i := len(path) - 1; i >= 0; i-- { + cur = map[string]interface{}{path[i]: cur} + } + return cur +} + +// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. +func processImportValues(c *chart.Chart, merge bool) error { + if c.Metadata.Dependencies == nil { + return nil + } + // combine chart values and empty config to get Values + var cvals common.Values + var err error + if merge { + cvals, err = util.MergeValues(c, nil) + } else { + cvals, err = util.CoalesceValues(c, nil) + } + if err != nil { + return err + } + b := make(map[string]interface{}) + // import values from each dependency if specified in import-values + for _, r := range c.Metadata.Dependencies { + var outiv []interface{} + for _, riv := range r.ImportValues { + switch iv := riv.(type) { + case map[string]interface{}: + child := fmt.Sprintf("%v", iv["child"]) + parent := fmt.Sprintf("%v", iv["parent"]) + + outiv = append(outiv, map[string]string{ + "child": child, + "parent": parent, + }) + + // get child table + vv, err := cvals.Table(r.Name + "." + child) + if err != nil { + slog.Warn( + "ImportValues missing table from chart", + slog.String("chart", "chart"), + slog.String("name", r.Name), + slog.Any("error", err), + ) + continue + } + // create value map from child to be merged into parent + if merge { + b = util.MergeTables(b, pathToMap(parent, vv.AsMap())) + } else { + b = util.CoalesceTables(b, pathToMap(parent, vv.AsMap())) + } + case string: + child := "exports." + iv + outiv = append(outiv, map[string]string{ + "child": child, + "parent": ".", + }) + vm, err := cvals.Table(r.Name + "." + child) + if err != nil { + slog.Warn("ImportValues missing table", slog.Any("error", err)) + continue + } + if merge { + b = util.MergeTables(b, vm.AsMap()) + } else { + b = util.CoalesceTables(b, vm.AsMap()) + } + } + } + r.ImportValues = outiv + } + + // Imported values from a child to a parent chart have a lower priority than + // the parents values. This enables parent charts to import a large section + // from a child and then override select parts. This is why b is merged into + // cvals in the code below and not the other way around. + if merge { + // deep copying the cvals as there are cases where pointers can end + // up in the cvals when they are copied onto b in ways that break things. + cvals = deepCopyMap(cvals) + c.Values = util.MergeTables(cvals, b) + } else { + // Trimming the nil values from cvals is needed for backwards compatibility. + // Previously, the b value had been populated with cvals along with some + // overrides. This caused the coalescing functionality to remove the + // nil/null values. This trimming is for backwards compat. + cvals = trimNilValues(cvals) + c.Values = util.CoalesceTables(cvals, b) + } + + return nil +} + +func deepCopyMap(vals map[string]interface{}) map[string]interface{} { + valsCopy, err := copystructure.Copy(vals) + if err != nil { + return vals + } + return valsCopy.(map[string]interface{}) +} + +func trimNilValues(vals map[string]interface{}) map[string]interface{} { + valsCopy, err := copystructure.Copy(vals) + if err != nil { + return vals + } + valsCopyMap := valsCopy.(map[string]interface{}) + for key, val := range valsCopyMap { + if val == nil { + // Iterate over the values and remove nil keys + delete(valsCopyMap, key) + } else if istable(val) { + // Recursively call into ourselves to remove keys from inner tables + valsCopyMap[key] = trimNilValues(val.(map[string]interface{})) + } + } + + return valsCopyMap +} + +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + +// processDependencyImportValues imports specified chart values from child to parent. +func processDependencyImportValues(c *chart.Chart, merge bool) error { + for _, d := range c.Dependencies() { + // recurse + if err := processDependencyImportValues(d, merge); err != nil { + return err + } + } + return processImportValues(c, merge) +} diff --git a/pkg/helm/intern/chart/v3/util/dependencies_test.go b/pkg/helm/intern/chart/v3/util/dependencies_test.go new file mode 100644 index 00000000..62b2725b --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/dependencies_test.go @@ -0,0 +1,571 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package util + +import ( + "context" + "os" + "path/filepath" + "sort" + "strconv" + "testing" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +func loadChart(t *testing.T, path string) *chart.Chart { + t.Helper() + c, err := loader.Load(context.Background(), path) + if err != nil { + t.Fatalf("failed to load testdata: %s", err) + } + return c +} + +func TestLoadDependency(t *testing.T) { + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + + check := func(deps []*chart.Dependency) { + if len(deps) != 2 { + t.Errorf("expected 2 dependencies, got %d", len(deps)) + } + for i, tt := range tests { + if deps[i].Name != tt.Name { + t.Errorf("expected dependency named %q, got %q", tt.Name, deps[i].Name) + } + if deps[i].Version != tt.Version { + t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version) + } + if deps[i].Repository != tt.Repository { + t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository) + } + } + } + c := loadChart(t, "testdata/frobnitz") + check(c.Metadata.Dependencies) + check(c.Lock.Dependencies) +} + +func TestDependencyEnabled(t *testing.T) { + type M = map[string]interface{} + tests := []struct { + name string + v M + e []string // expected charts including duplicates in alphanumeric order + }{{ + "tags with no effect", + M{"tags": M{"nothinguseful": false}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, + }, { + "tags disabling a group", + M{"tags": M{"front-end": false}}, + []string{"parentchart"}, + }, { + "tags disabling a group and enabling a different group", + M{"tags": M{"front-end": false, "back-end": true}}, + []string{"parentchart", "parentchart.subchart2", "parentchart.subchart2.subchartb", "parentchart.subchart2.subchartc"}, + }, { + "tags disabling only children, children still enabled since tag front-end=true in values.yaml", + M{"tags": M{"subcharta": false, "subchartb": false}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, + }, { + "tags disabling all parents/children with additional tag re-enabling a parent", + M{"tags": M{"front-end": false, "subchart1": true, "back-end": false}}, + []string{"parentchart", "parentchart.subchart1"}, + }, { + "conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml", + M{"subchart1": M{"enabled": true}, "subchart2": M{"enabled": true}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2"}, + }, { + "conditions disabling the parent charts, effectively disabling children", + M{"subchart1": M{"enabled": false}, "subchart2": M{"enabled": false}}, + []string{"parentchart"}, + }, { + "conditions a child using the second condition path of child's condition", + M{"subchart1": M{"subcharta": M{"enabled": false}}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subchartb"}, + }, { + "tags enabling a parent/child group with condition disabling one child", + M{"subchart2": M{"subchartc": M{"enabled": false}}, "tags": M{"back-end": true}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2", "parentchart.subchart2.subchartb"}, + }, { + "tags will not enable a child if parent is explicitly disabled with condition", + M{"subchart1": M{"enabled": false}, "tags": M{"front-end": true}}, + []string{"parentchart"}, + }, { + "subcharts with alias also respect conditions", + M{"subchart1": M{"enabled": false}, "subchart2alias": M{"enabled": true, "subchartb": M{"enabled": true}}}, + []string{"parentchart", "parentchart.subchart2alias", "parentchart.subchart2alias.subchartb"}, + }} + + for _, tc := range tests { + c := loadChart(t, "testdata/subpop") + t.Run(tc.name, func(t *testing.T) { + if err := processDependencyEnabled(c, tc.v, ""); err != nil { + t.Fatalf("error processing enabled dependencies %v", err) + } + + names := extractChartNames(c) + if len(names) != len(tc.e) { + t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e)) + } + for i := range names { + if names[i] != tc.e[i] { + t.Fatalf("slice values do not match got %v, expected %v", names, tc.e) + } + } + }) + } +} + +// extractChartNames recursively searches chart dependencies returning all charts found +func extractChartNames(c *chart.Chart) []string { + var out []string + var fn func(c *chart.Chart) + fn = func(c *chart.Chart) { + out = append(out, c.ChartPath()) + for _, d := range c.Dependencies() { + fn(d) + } + } + fn(c) + sort.Strings(out) + return out +} + +func TestProcessDependencyImportValues(t *testing.T) { + c := loadChart(t, "testdata/subpop") + + e := make(map[string]string) + + e["imported-chart1.SC1bool"] = "true" + e["imported-chart1.SC1float"] = "3.14" + e["imported-chart1.SC1int"] = "100" + e["imported-chart1.SC1string"] = "dollywood" + e["imported-chart1.SC1extra1"] = "11" + e["imported-chart1.SPextra1"] = "helm rocks" + e["imported-chart1.SC1extra1"] = "11" + + e["imported-chartA.SCAbool"] = "false" + e["imported-chartA.SCAfloat"] = "3.1" + e["imported-chartA.SCAint"] = "55" + e["imported-chartA.SCAstring"] = "jabba" + e["imported-chartA.SPextra3"] = "1.337" + e["imported-chartA.SC1extra2"] = "1.337" + e["imported-chartA.SCAnested1.SCAnested2"] = "true" + + e["imported-chartA-B.SCAbool"] = "false" + e["imported-chartA-B.SCAfloat"] = "3.1" + e["imported-chartA-B.SCAint"] = "55" + e["imported-chartA-B.SCAstring"] = "jabba" + + e["imported-chartA-B.SCBbool"] = "true" + e["imported-chartA-B.SCBfloat"] = "7.77" + e["imported-chartA-B.SCBint"] = "33" + e["imported-chartA-B.SCBstring"] = "boba" + e["imported-chartA-B.SPextra5"] = "k8s" + e["imported-chartA-B.SC1extra5"] = "tiller" + + // These values are imported from the child chart to the parent. Parent + // values take precedence over imported values. This enables importing a + // large section from a child chart and overriding a selection from it. + e["overridden-chart1.SC1bool"] = "false" + e["overridden-chart1.SC1float"] = "3.141592" + e["overridden-chart1.SC1int"] = "99" + e["overridden-chart1.SC1string"] = "pollywog" + e["overridden-chart1.SPextra2"] = "42" + + e["overridden-chartA.SCAbool"] = "true" + e["overridden-chartA.SCAfloat"] = "41.3" + e["overridden-chartA.SCAint"] = "808" + e["overridden-chartA.SCAstring"] = "jabberwocky" + e["overridden-chartA.SPextra4"] = "true" + + // These values are imported from the child chart to the parent. Parent + // values take precedence over imported values. This enables importing a + // large section from a child chart and overriding a selection from it. + e["overridden-chartA-B.SCAbool"] = "true" + e["overridden-chartA-B.SCAfloat"] = "41.3" + e["overridden-chartA-B.SCAint"] = "808" + e["overridden-chartA-B.SCAstring"] = "jabberwocky" + e["overridden-chartA-B.SCBbool"] = "false" + e["overridden-chartA-B.SCBfloat"] = "1.99" + e["overridden-chartA-B.SCBint"] = "77" + e["overridden-chartA-B.SCBstring"] = "jango" + e["overridden-chartA-B.SPextra6"] = "111" + e["overridden-chartA-B.SCAextra1"] = "23" + e["overridden-chartA-B.SCBextra1"] = "13" + e["overridden-chartA-B.SC1extra6"] = "77" + + // `exports` style + e["SCBexported1B"] = "1965" + e["SC1extra7"] = "true" + e["SCBexported2A"] = "blaster" + e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" + + if err := processDependencyImportValues(c, false); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc := common.Values(c.Values) + for kk, vv := range e { + pv, err := cc.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + + switch pv := pv.(type) { + case float64: + if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { + t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk) + } + case bool: + if b := strconv.FormatBool(pv); b != vv { + t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk) + } + default: + if pv != vv { + t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk) + } + } + } + + // Since this was processed with coalescing there should be no null values. + // Here we verify that. + _, err := cc.PathValue("ensurenull") + if err == nil { + t.Error("expect nil value not found but found it") + } + switch xerr := err.(type) { + case common.ErrNoValue: + // We found what we expected + default: + t.Errorf("expected an ErrNoValue but got %q instead", xerr) + } + + c = loadChart(t, "testdata/subpop") + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc = common.Values(c.Values) + val, err := cc.PathValue("ensurenull") + if err != nil { + t.Error("expect value but ensurenull was not found") + } + if val != nil { + t.Errorf("expect nil value but got %q instead", val) + } +} + +func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) { + c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies") + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + e := make(map[string]string) + + e["foo-defaults.defaultValue"] = "42" + e["bar-defaults.defaultValue"] = "42" + + e["foo.defaults.defaultValue"] = "42" + e["bar.defaults.defaultValue"] = "42" + + e["foo.grandchild.defaults.defaultValue"] = "42" + e["bar.grandchild.defaults.defaultValue"] = "42" + + cValues := common.Values(c.Values) + for kk, vv := range e { + pv, err := cValues.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + if pv != vv { + t.Errorf("failed to match imported value %v with expected %v", pv, vv) + } + } +} + +func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { + c := loadChart(t, "testdata/three-level-dependent-chart/umbrella") + + e := make(map[string]string) + + // The order of precedence should be: + // 1. User specified values (e.g CLI) + // 2. Parent chart values + // 3. Imported values + // 4. Sub-chart values + // The 4 app charts here deal with things differently: + // - app1 has a port value set in the umbrella chart. It does not import any + // values so the value from the umbrella chart should be used. + // - app2 has a value in the app chart and imports from the library. The + // app chart value should take precedence. + // - app3 has no value in the app chart and imports the value from the library + // chart. The library chart value should be used. + // - app4 has a value in the app chart and does not import the value from the + // library chart. The app charts value should be used. + e["app1.service.port"] = "3456" + e["app2.service.port"] = "8080" + e["app3.service.port"] = "9090" + e["app4.service.port"] = "1234" + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc := common.Values(c.Values) + for kk, vv := range e { + pv, err := cc.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + + switch pv := pv.(type) { + case float64: + if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { + t.Errorf("failed to match imported float value %v with expected %v", s, vv) + } + default: + if pv != vv { + t.Errorf("failed to match imported string value %q with expected %q", pv, vv) + } + } + } +} + +func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) { + c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") + nameOverride := "parent-chart-prod" + + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 1 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } + + prodDependencyValues := c.Dependencies()[0].Values + if prodDependencyValues["nameOverride"] != nameOverride { + t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"]) + } +} + +func TestGetAliasDependency(t *testing.T) { + c := loadChart(t, "testdata/frobnitz") + req := c.Metadata.Dependencies + + if len(req) == 0 { + t.Fatalf("there are no dependencies to test") + } + + // Success case + aliasChart := getAliasDependency(c.Dependencies(), req[0]) + if aliasChart == nil { + t.Fatalf("failed to get dependency chart for alias %s", req[0].Name) + } + if req[0].Alias != "" { + if aliasChart.Name() != req[0].Alias { + t.Fatalf("dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name()) + } + } else if aliasChart.Name() != req[0].Name { + t.Fatalf("dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name()) + } + + if req[0].Version != "" { + if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { + t.Fatalf("dependency chart version is not in the compatible range") + } + } + + // Failure case + req[0].Name = "something-else" + if aliasChart := getAliasDependency(c.Dependencies(), req[0]); aliasChart != nil { + t.Fatalf("expected no chart but got %s", aliasChart.Name()) + } + + req[0].Version = "something else which is not in the compatible range" + if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { + t.Fatalf("dependency chart version which is not in the compatible range should cause a failure other than a success ") + } +} + +func TestDependentChartAliases(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-alias") + req := c.Metadata.Dependencies + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 3 { + t.Fatal("expected alias dependencies to be added") + } + + if len(c.Dependencies()) != len(c.Metadata.Dependencies) { + t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) + } + + aliasChart := getAliasDependency(c.Dependencies(), req[2]) + + if aliasChart == nil { + t.Fatalf("failed to get dependency chart for alias %s", req[2].Name) + } + if aliasChart.Parent() != c { + t.Fatalf("dependency chart has wrong parent, expected %s but got %s", c.Name(), aliasChart.Parent().Name()) + } + if req[2].Alias != "" { + if aliasChart.Name() != req[2].Alias { + t.Fatalf("dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name()) + } + } else if aliasChart.Name() != req[2].Name { + t.Fatalf("dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name()) + } + + req[2].Name = "dummy-name" + if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil { + t.Fatalf("expected no chart but got %s", aliasChart.Name()) + } + +} + +func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-no-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } +} + +func TestDependentChartWithSubChartsHelmignore(t *testing.T) { + // FIXME what does this test? + loadChart(t, "testdata/dependent-chart-helmignore") +} + +func TestDependentChartsWithSubChartsSymlink(t *testing.T) { + joonix := filepath.Join("testdata", "joonix") + if err := os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(filepath.Join(joonix, "charts", "frobnitz")) + c := loadChart(t, joonix) + + if c.Name() != "joonix" { + t.Fatalf("unexpected chart name: %s", c.Name()) + } + if n := len(c.Dependencies()); n != 1 { + t.Fatalf("expected 1 dependency for this chart, but got %d", n) + } +} + +func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-with-all-in-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Dependencies()) != len(c.Metadata.Dependencies) { + t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) + } +} + +func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-with-mixed-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } +} + +func validateDependencyTree(t *testing.T, c *chart.Chart) { + t.Helper() + for _, dependency := range c.Dependencies() { + if dependency.Parent() != c { + if dependency.Parent() != c { + t.Fatalf("dependency chart %s has wrong parent, expected %s but got %s", dependency.Name(), c.Name(), dependency.Parent().Name()) + } + } + // recurse entire tree + validateDependencyTree(t, dependency) + } +} + +func TestChartWithDependencyAliasedTwiceAndDoublyReferencedSubDependency(t *testing.T) { + c := loadChart(t, "testdata/chart-with-dependency-aliased-twice") + + if len(c.Dependencies()) != 1 { + t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected two dependencies after processing aliases") + } + validateDependencyTree(t, c) +} diff --git a/pkg/helm/intern/chart/v3/util/doc.go b/pkg/helm/intern/chart/v3/util/doc.go new file mode 100644 index 00000000..9a71f056 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/doc.go @@ -0,0 +1,45 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +package util contains tools for working with charts. + +Charts are described in the chart package (pkg/chart). +This package provides utilities for serializing and deserializing charts. + +A chart can be represented on the file system in one of two ways: + + - As a directory that contains a Chart.yaml file and other chart things. + - As a tarred gzipped file containing a directory that then contains a + Chart.yaml file. + +This package provides utilities for working with those file formats. + +The preferred way of loading a chart is using 'loader.Load`: + + chart, err := loader.Load(filename) + +This will attempt to discover whether the file at 'filename' is a directory or +a chart archive. It will then load accordingly. + +For accepting raw compressed tar file data from an io.Reader, the +'loader.LoadArchive()' will read in the data, uncompress it, and unpack it +into a Chart. + +When creating charts in memory, use the 'github.com/werf/nelm/pkg/helm/pkg/chart' +package directly. +*/ +package util // import chartutil "github.com/werf/nelm/pkg/helm/intern/chart/v3/util" diff --git a/pkg/helm/intern/chart/v3/util/expand.go b/pkg/helm/intern/chart/v3/util/expand.go new file mode 100644 index 00000000..7a9584bf --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/expand.go @@ -0,0 +1,94 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + securejoin "github.com/cyphar/filepath-securejoin" + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" +) + +// Expand uncompresses and extracts a chart into the specified directory. +func Expand(dir string, r io.Reader) error { + files, err := archive.LoadArchiveFiles(r) + if err != nil { + return err + } + + // Get the name of the chart + var chartName string + for _, file := range files { + if file.Name == "Chart.yaml" { + ch := &chart.Metadata{} + if err := yaml.Unmarshal(file.Data, ch); err != nil { + return fmt.Errorf("cannot load Chart.yaml: %w", err) + } + chartName = ch.Name + } + } + if chartName == "" { + return errors.New("chart name not specified") + } + + // Find the base directory + // The directory needs to be cleaned prior to passing to SecureJoin or the location may end up + // being wrong or returning an error. This was introduced in v0.4.0. + dir = filepath.Clean(dir) + chartdir, err := securejoin.SecureJoin(dir, chartName) + if err != nil { + return err + } + + // Copy all files verbatim. We don't parse these files because parsing can remove + // comments. + for _, file := range files { + outpath, err := securejoin.SecureJoin(chartdir, file.Name) + if err != nil { + return err + } + + // Make sure the necessary subdirs get created. + basedir := filepath.Dir(outpath) + if err := os.MkdirAll(basedir, 0755); err != nil { + return err + } + + if err := os.WriteFile(outpath, file.Data, 0644); err != nil { + return err + } + } + + return nil +} + +// ExpandFile expands the src file into the dest directory. +func ExpandFile(dest, src string) error { + h, err := os.Open(src) + if err != nil { + return err + } + defer h.Close() + return Expand(dest, h) +} diff --git a/pkg/helm/intern/chart/v3/util/expand_test.go b/pkg/helm/intern/chart/v3/util/expand_test.go new file mode 100644 index 00000000..280995f7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/expand_test.go @@ -0,0 +1,124 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "os" + "path/filepath" + "testing" +) + +func TestExpand(t *testing.T) { + dest := t.TempDir() + + reader, err := os.Open("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatal(err) + } + + if err := Expand(dest, reader); err != nil { + t.Fatal(err) + } + + expectedChartPath := filepath.Join(dest, "frobnitz") + fi, err := os.Stat(expectedChartPath) + if err != nil { + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("expected a chart directory at %s", expectedChartPath) + } + + dir, err := os.Open(expectedChartPath) + if err != nil { + t.Fatal(err) + } + + fis, err := dir.Readdir(0) + if err != nil { + t.Fatal(err) + } + + expectLen := 11 + if len(fis) != expectLen { + t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) + } + + for _, fi := range fis { + expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) + if err != nil { + t.Fatal(err) + } + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat always returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } + } + } +} + +func TestExpandFile(t *testing.T) { + dest := t.TempDir() + + if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil { + t.Fatal(err) + } + + expectedChartPath := filepath.Join(dest, "frobnitz") + fi, err := os.Stat(expectedChartPath) + if err != nil { + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("expected a chart directory at %s", expectedChartPath) + } + + dir, err := os.Open(expectedChartPath) + if err != nil { + t.Fatal(err) + } + + fis, err := dir.Readdir(0) + if err != nil { + t.Fatal(err) + } + + expectLen := 11 + if len(fis) != expectLen { + t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) + } + + for _, fi := range fis { + expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) + if err != nil { + t.Fatal(err) + } + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat always returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } + } + } +} diff --git a/pkg/helm/intern/chart/v3/util/save.go b/pkg/helm/intern/chart/v3/util/save.go new file mode 100644 index 00000000..4be195c4 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/save.go @@ -0,0 +1,257 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "time" + + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") + +// SaveDir saves a chart as files in a directory. +// +// This takes the chart name, and creates a new subdirectory inside of the given dest +// directory, writing the chart's contents to that subdirectory. +func SaveDir(c *chart.Chart, dest string) error { + // Create the chart directory + err := validateName(c.Name()) + if err != nil { + return err + } + outdir := filepath.Join(dest, c.Name()) + if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() { + return fmt.Errorf("file %s already exists and is not a directory", outdir) + } + if err := os.MkdirAll(outdir, 0755); err != nil { + return err + } + + // Save the chart file. + if err := SaveChartfile(filepath.Join(outdir, ChartfileName), c.Metadata); err != nil { + return err + } + + // Save values.yaml + for _, f := range c.Raw { + if f.Name == ValuesfileName { + vf := filepath.Join(outdir, ValuesfileName) + if err := writeFile(vf, f.Data); err != nil { + return err + } + } + } + + // Save values.schema.json if it exists + if c.Schema != nil { + filename := filepath.Join(outdir, SchemafileName) + if err := writeFile(filename, c.Schema); err != nil { + return err + } + } + + // Save templates and files + for _, o := range [][]*common.File{c.Templates, c.Files} { + for _, f := range o { + n := filepath.Join(outdir, f.Name) + if err := writeFile(n, f.Data); err != nil { + return err + } + } + } + + // Save dependencies + base := filepath.Join(outdir, ChartsDir) + for _, dep := range c.Dependencies() { + // Here, we write each dependency as a tar file. + if _, err := Save(dep, base); err != nil { + return fmt.Errorf("saving %s: %w", dep.ChartFullPath(), err) + } + } + return nil +} + +// Save creates an archived chart to the given directory. +// +// This takes an existing chart and a destination directory. +// +// If the directory is /foo, and the chart is named bar, with version 1.0.0, this +// will generate /foo/bar-1.0.0.tgz. +// +// This returns the absolute path to the chart archive file. +func Save(c *chart.Chart, outDir string) (string, error) { + if err := c.Validate(); err != nil { + return "", fmt.Errorf("chart validation: %w", err) + } + + filename := fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version) + filename = filepath.Join(outDir, filename) + dir := filepath.Dir(filename) + if stat, err := os.Stat(dir); err != nil { + if errors.Is(err, fs.ErrNotExist) { + if err2 := os.MkdirAll(dir, 0755); err2 != nil { + return "", err2 + } + } else { + return "", fmt.Errorf("stat %s: %w", dir, err) + } + } else if !stat.IsDir() { + return "", fmt.Errorf("is not a directory: %s", dir) + } + + f, err := os.Create(filename) + if err != nil { + return "", err + } + + // Wrap in gzip writer + zipper := gzip.NewWriter(f) + zipper.Extra = headerBytes + zipper.Comment = "Helm" + + // Wrap in tar writer + twriter := tar.NewWriter(zipper) + rollback := false + defer func() { + twriter.Close() + zipper.Close() + f.Close() + if rollback { + os.Remove(filename) + } + }() + + if err := writeTarContents(twriter, c, ""); err != nil { + rollback = true + return filename, err + } + return filename, nil +} + +func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { + err := validateName(c.Name()) + if err != nil { + return err + } + base := filepath.Join(prefix, c.Name()) + + // Save Chart.yaml + cdata, err := yaml.Marshal(c.Metadata) + if err != nil { + return err + } + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil { + return err + } + + // Save Chart.lock + if c.Lock != nil { + ldata, err := yaml.Marshal(c.Lock) + if err != nil { + return err + } + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil { + return err + } + } + + // Save values.yaml + for _, f := range c.Raw { + if f.Name == ValuesfileName { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil { + return err + } + } + } + + // Save values.schema.json if it exists + if c.Schema != nil { + if !json.Valid(c.Schema) { + return errors.New("invalid JSON in " + SchemafileName) + } + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil { + return err + } + } + + // Save templates + for _, f := range c.Templates { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { + return err + } + } + + // Save files + for _, f := range c.Files { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { + return err + } + } + + // Save dependencies + for _, dep := range c.Dependencies() { + if err := writeTarContents(out, dep, filepath.Join(base, ChartsDir)); err != nil { + return err + } + } + return nil +} + +// writeToTar writes a single file to a tar archive. +func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error { + // TODO: Do we need to create dummy parent directory names if none exist? + h := &tar.Header{ + Name: filepath.ToSlash(name), + Mode: 0644, + Size: int64(len(body)), + ModTime: modTime, + } + if h.ModTime.IsZero() { + h.ModTime = time.Now() + } + if err := out.WriteHeader(h); err != nil { + return err + } + _, err := out.Write(body) + return err +} + +// If the name has directory name has characters which would change the location +// they need to be removed. +func validateName(name string) error { + nname := filepath.Base(name) + + if nname != name { + return common.ErrInvalidChartName{Name: name} + } + + return nil +} diff --git a/pkg/helm/intern/chart/v3/util/save_test.go b/pkg/helm/intern/chart/v3/util/save_test.go new file mode 100644 index 00000000..b7d8abec --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/save_test.go @@ -0,0 +1,358 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +func TestSave(t *testing.T) { + tmp := t.TempDir() + + for _, dest := range []string{tmp, filepath.Join(tmp, "newdir")} { + t.Run("outDir="+dest, func(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + } + chartWithInvalidJSON := withSchema(*c, []byte("{")) + + where, err := Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + if !strings.HasPrefix(where, dest) { + t.Fatalf("Expected %q to start with %q", where, dest) + } + if !strings.HasSuffix(where, ".tgz") { + t.Fatalf("Expected %q to end with .tgz", where) + } + + c2, err := loader.LoadFile(context.Background(), where) + if err != nil { + t.Fatal(err) + } + if c2.Name() != c.Name() { + t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) + } + if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { + t.Fatal("Files data did not match") + } + + if !bytes.Equal(c.Schema, c2.Schema) { + indentation := 4 + formattedExpected := Indent(indentation, string(c.Schema)) + formattedActual := Indent(indentation, string(c2.Schema)) + t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual) + } + if _, err := Save(&chartWithInvalidJSON, dest); err == nil { + t.Fatalf("Invalid JSON was not caught while saving chart") + } + + c.Metadata.APIVersion = chart.APIVersionV3 + where, err = Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + c2, err = loader.LoadFile(context.Background(), where) + if err != nil { + t.Fatal(err) + } + if c2.Lock == nil { + t.Fatal("Expected v3 chart archive to contain a Chart.lock file") + } + if c2.Lock.Digest != c.Lock.Digest { + t.Fatal("Chart.lock data did not match") + } + }) + } + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "../ahab", + Version: "1.2.3", + }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, + }, + } + _, err := Save(c, tmp) + if err == nil { + t.Fatal("Expected error saving chart with invalid name") + } +} + +// Creates a copy with a different schema; does not modify anything. +func withSchema(chart chart.Chart, schema []byte) chart.Chart { + chart.Schema = schema + return chart +} + +func Indent(n int, text string) string { + startOfLine := regexp.MustCompile(`(?m)^`) + indentation := strings.Repeat(" ", n) + return startOfLine.ReplaceAllLiteralString(text, indentation) +} + +func TestSavePreservesTimestamps(t *testing.T) { + // Test executes so quickly that if we don't subtract a second, the + // check will fail because `initialCreateTime` will be identical to the + // written timestamp for the files. + initialCreateTime := time.Now().Add(-1 * time.Second) + tmp := t.TempDir() + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: initialCreateTime, + Values: map[string]interface{}{ + "imageName": "testimage", + "imageId": 42, + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: initialCreateTime, + } + + where, err := Save(c, tmp) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + + allHeaders, err := retrieveAllHeadersFromTar(where) + if err != nil { + t.Fatalf("Failed to parse tar: %v", err) + } + + roundedTime := initialCreateTime.Round(time.Second) + for _, header := range allHeaders { + if !header.ModTime.Equal(roundedTime) { + t.Fatalf("File timestamp not preserved: %v", header.ModTime) + } + } +} + +// We could refactor `load.go` to use this `retrieveAllHeadersFromTar` function +// as well, so we are not duplicating components of the code which iterate +// through the tar. +func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { + raw, err := os.Open(path) + if err != nil { + return nil, err + } + defer raw.Close() + + unzipped, err := gzip.NewReader(raw) + if err != nil { + return nil, err + } + defer unzipped.Close() + + tr := tar.NewReader(unzipped) + headers := []*tar.Header{} + for { + hd, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, err + } + + headers = append(headers, hd) + } + + return headers, nil +} + +func TestSaveDir(t *testing.T) { + tmp := t.TempDir() + modTime := time.Now() + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Templates: []*common.File{ + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")}, + }, + } + + if err := SaveDir(c, tmp); err != nil { + t.Fatalf("Failed to save: %s", err) + } + + c2, err := loader.LoadDir(context.Background(), tmp+"/ahab") + if err != nil { + t.Fatal(err) + } + + if c2.Name() != c.Name() { + t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) + } + + if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name { + t.Fatal("Templates data did not match") + } + + if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name { + t.Fatal("Files data did not match") + } + + tmp2 := t.TempDir() + c.Metadata.Name = "../ahab" + pth := filepath.Join(tmp2, "tmpcharts") + if err := os.MkdirAll(filepath.Join(pth), 0755); err != nil { + t.Fatal(err) + } + + if err := SaveDir(c, pth); err.Error() != "\"../ahab\" is not a valid chart name" { + t.Fatalf("Did not get expected error for chart named %q", c.Name()) + } +} + +func TestRepeatableSave(t *testing.T) { + tmp := t.TempDir() + defer os.RemoveAll(tmp) + modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC) + tests := []struct { + name string + chart *chart.Chart + want string + }{ + { + name: "Package 1 file", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + Generated: modTime, + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "5bfea18cc3c8cbc265744bc32bffa9489a4dbe87d6b51b90f4255e4839d35e03", + }, + { + name: "Package 2 files", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + Generated: modTime, + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + {Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "a240365c21e0a2f4a57873132a9b686566a612d08bcb3f20c9446bfff005ccce", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create package + dest := path.Join(tmp, "newdir") + where, err := Save(test.chart, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + // get shasum for package + result, err := sha256Sum(where) + if err != nil { + t.Fatalf("Failed to check shasum: %s", err) + } + // assert that the package SHA is what we wanted. + if result != test.want { + t.Errorf("FormatName() result = %v, want %v", result, test.want) + } + }) + } +} + +func sha256Sum(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml new file mode 100644 index 00000000..4a4da799 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v3 +appVersion: 1.0.0 +name: chart-with-dependency-aliased-twice +type: application +version: 1.0.0 + +dependencies: + - name: child + alias: foo + version: 1.0.0 + - name: child + alias: bar + version: 1.0.0 + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml new file mode 100644 index 00000000..0f3afd8c --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +appVersion: 1.0.0 +name: child +type: application +version: 1.0.0 + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml new file mode 100644 index 00000000..3e0bf725 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +appVersion: 1.0.0 +name: grandchild +type: application +version: 1.0.0 + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml new file mode 100644 index 00000000..1830492e --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }}-{{ .Values.from }} +data: + {{- toYaml .Values | nindent 2 }} + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml new file mode 100644 index 00000000..b5d55af7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }} +data: + {{- toYaml .Values | nindent 2 }} + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml new file mode 100644 index 00000000..695521a4 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml @@ -0,0 +1,7 @@ +foo: + grandchild: + from: foo +bar: + grandchild: + from: bar + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml new file mode 100644 index 00000000..f2f0610b --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v3 +appVersion: 1.0.0 +name: chart-with-dependency-aliased-twice +type: application +version: 1.0.0 + +dependencies: + - name: child + alias: foo + version: 1.0.0 + import-values: + - parent: foo-defaults + child: defaults + - name: child + alias: bar + version: 1.0.0 + import-values: + - parent: bar-defaults + child: defaults + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml new file mode 100644 index 00000000..08ccac9e --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v3 +appVersion: 1.0.0 +name: child +type: application +version: 1.0.0 + +dependencies: + - name: grandchild + version: 1.0.0 + import-values: + - parent: defaults + child: defaults diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml new file mode 100644 index 00000000..3e0bf725 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v3 +appVersion: 1.0.0 +name: grandchild +type: application +version: 1.0.0 + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml new file mode 100644 index 00000000..f51c594f --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml @@ -0,0 +1,2 @@ +defaults: + defaultValue: "42" \ No newline at end of file diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml new file mode 100644 index 00000000..3140f53d --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }} +data: + {{ .Values.defaults | toYaml }} + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml new file mode 100644 index 00000000..a2b62c95 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }} +data: + {{ toYaml .Values.defaults | indent 2 }} + diff --git a/pkg/helm/intern/chart/v3/util/testdata/chartfiletest.yaml b/pkg/helm/intern/chart/v3/util/testdata/chartfiletest.yaml new file mode 100644 index 00000000..d222c8f8 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/chartfiletest.yaml @@ -0,0 +1,20 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue diff --git a/pkg/helm/pkg/chartutil/testdata/coleridge.yaml b/pkg/helm/intern/chart/v3/util/testdata/coleridge.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/coleridge.yaml rename to pkg/helm/intern/chart/v3/util/testdata/coleridge.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/.helmignore b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/.helmignore similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/.helmignore rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/.helmignore diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.lock b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/Chart.lock similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.lock rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/Chart.lock diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml new file mode 100644 index 00000000..b8773d0d --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml @@ -0,0 +1,29 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts + alias: mariners2 + - name: mariner + version: "4.3.2" + repository: https://example.com/charts + alias: mariners1 diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/INSTALL.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/INSTALL.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/LICENSE b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/LICENSE similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/LICENSE rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/LICENSE diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/_ignore_me b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/_ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/docs/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/docs/README.md similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/docs/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/docs/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/icon.svg b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/icon.svg similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/icon.svg rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/icon.svg diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/ignore/me.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/ignore/me.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/templates/template.tpl b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/templates/template.tpl rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/values.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-alias/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/.helmignore b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/.helmignore rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml new file mode 100644 index 00000000..8b4ad8cd --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/.ignore_me b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/.ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/_ignore_me b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/_ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/templates/template.tpl b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/templates/template.tpl rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/.helmignore b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/.helmignore rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml new file mode 100644 index 00000000..8b4ad8cd --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml @@ -0,0 +1,17 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/INSTALL.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/INSTALL.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/LICENSE b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/LICENSE rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/_ignore_me b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/_ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/docs/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/docs/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/icon.svg b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/icon.svg rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/ignore/me.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/ignore/me.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/templates/template.tpl b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/templates/template.tpl rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/.helmignore b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/.helmignore rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml new file mode 100644 index 00000000..06283093 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/LICENSE b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/LICENSE rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz old mode 100755 new mode 100644 similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/docs/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/docs/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/icon.svg b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/icon.svg rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml new file mode 100644 index 00000000..6543799d --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml @@ -0,0 +1,21 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz-1.2.3.tgz b/pkg/helm/intern/chart/v3/util/testdata/frobnitz-1.2.3.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz-1.2.3.tgz rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz-1.2.3.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/.helmignore similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/.helmignore diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/Chart.lock b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/Chart.lock similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/Chart.lock rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/Chart.lock diff --git a/pkg/helm/intern/chart/v3/util/testdata/frobnitz/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/Chart.yaml new file mode 100644 index 00000000..1b63fc3e --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/Chart.yaml @@ -0,0 +1,27 @@ +apiVersion: v3 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +annotations: + extrakey: extravalue + anotherkey: anothervalue +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/INSTALL.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/LICENSE similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/LICENSE diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/README.md b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/README.md rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/_ignore_me diff --git a/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml new file mode 100644 index 00000000..2a2c9c88 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: alpine +description: Deploy a basic Alpine Linux pod +version: 0.1.0 +home: https://helm.sh/helm diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 00000000..aea109c7 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: mast1 +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml new file mode 100644 index 00000000..4d3eea73 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v3 +name: mariner +description: A Helm chart for Kubernetes +version: 4.3.2 +home: "" +dependencies: + - name: albatross + repository: https://example.com/mariner/charts + version: "0.1.0" diff --git a/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml new file mode 100644 index 00000000..da605991 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: albatross +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/templates/placeholder.tpl b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/templates/placeholder.tpl rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/docs/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/docs/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/icon.svg similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/icon.svg diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/ignore/me.txt diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/templates/template.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/frobnitz/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/helm/intern/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz_backslash-1.2.3.tgz rename to pkg/helm/intern/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/genfrob.sh b/pkg/helm/intern/chart/v3/util/testdata/genfrob.sh similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/genfrob.sh rename to pkg/helm/intern/chart/v3/util/testdata/genfrob.sh diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock diff --git a/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml new file mode 100644 index 00000000..0b3e9958 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v3 +name: parent-chart +version: v0.1.0 +appVersion: v0.1.0 +dependencies: + - name: dev + repository: "file://envs/dev" + version: ">= 0.0.1" + condition: dev.enabled,global.dev.enabled + tags: + - dev + import-values: + - data + + - name: prod + repository: "file://envs/prod" + version: ">= 0.0.1" + condition: prod.enabled,global.prod.enabled + tags: + - prod + import-values: + - data \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz diff --git a/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml new file mode 100644 index 00000000..72427c09 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +name: dev +version: v0.1.0 +appVersion: v0.1.0 \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml new file mode 100644 index 00000000..058ab394 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +name: prod +version: v0.1.0 +appVersion: v0.1.0 \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/joonix/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/joonix/Chart.yaml new file mode 100644 index 00000000..1860a3df --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/joonix/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: joonix +version: 1.2.3 diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml b/pkg/helm/intern/chart/v3/util/testdata/joonix/charts/.gitkeep similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml rename to pkg/helm/intern/chart/v3/util/testdata/joonix/charts/.gitkeep diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/Chart.yaml new file mode 100644 index 00000000..53e9ec50 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/Chart.yaml @@ -0,0 +1,41 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: parentchart +version: 0.1.0 +dependencies: + - name: subchart1 + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart1.enabled + tags: + - front-end + - subchart1 + import-values: + - child: SC1data + parent: imported-chart1 + - child: SC1data + parent: overridden-chart1 + - child: imported-chartA + parent: imported-chartA + - child: imported-chartA-B + parent: imported-chartA-B + - child: overridden-chartA-B + parent: overridden-chartA-B + - child: SCBexported1A + parent: . + - SCBexported2 + - SC1exported1 + + - name: subchart2 + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart2.enabled + tags: + - back-end + - subchart2 + + - name: subchart2 + alias: subchart2alias + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart2alias.enabled diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/README.md b/pkg/helm/intern/chart/v3/util/testdata/subpop/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/README.md rename to pkg/helm/intern/chart/v3/util/testdata/subpop/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml new file mode 100644 index 00000000..1539fb97 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml @@ -0,0 +1,36 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: subchart1 +version: 0.1.0 +dependencies: + - name: subcharta + repository: http://localhost:10191 + version: 0.1.0 + condition: subcharta.enabled + tags: + - front-end + - subcharta + import-values: + - child: SCAdata + parent: imported-chartA + - child: SCAdata + parent: overridden-chartA + - child: SCAdata + parent: imported-chartA-B + + - name: subchartb + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartb.enabled + import-values: + - child: SCBdata + parent: imported-chartB + - child: SCBdata + parent: imported-chartA-B + - child: exports.SCBexported2 + parent: exports.SCBexported2 + - SCBexported1 + + tags: + - front-end + - subchartb diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml new file mode 100644 index 00000000..2755a821 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: subcharta +version: 0.1.0 diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartA/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartA/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml new file mode 100644 index 00000000..bf12fe8f --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: subchartb +version: 0.1.0 diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/NOTES.txt b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/NOTES.txt rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/role.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml new file mode 100644 index 00000000..a974e316 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml @@ -0,0 +1,55 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# subchart1 +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 + + +SC1data: + SC1bool: true + SC1float: 3.14 + SC1int: 100 + SC1string: "dollywood" + SC1extra1: 11 + +imported-chartA: + SC1extra2: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 3.14 + SCAint: 100 + SCAstring: "jabbathehut" + SC1extra3: true + +imported-chartA-B: + SC1extra5: "tiller" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 3.33 + SCAint: 555 + SCAstring: "wormwood" + SCAextra1: 23 + + SCBbool: true + SCBfloat: 0.25 + SCBint: 98 + SCBstring: "murkwood" + SCBextra1: 13 + + SC1extra6: 77 + +SCBexported1A: + SC1extra7: true + +exports: + SC1exported1: + global: + SC1exported2: + all: + SC1exported3: "SC1expstr" \ No newline at end of file diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml new file mode 100644 index 00000000..e7765704 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: subchart2 +version: 0.1.0 +dependencies: + - name: subchartb + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartb.enabled + tags: + - back-end + - subchartb + - name: subchartc + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartc.enabled + tags: + - back-end + - subchartc diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml new file mode 100644 index 00000000..bf12fe8f --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: subchartb +version: 0.1.0 diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml new file mode 100644 index 00000000..fb3dfc44 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: subchart2-{{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: subchart2-{{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml new file mode 100644 index 00000000..e8c0ef5e --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: subchartc +version: 0.1.0 diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/Chart.yaml new file mode 100644 index 00000000..09eb05a9 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v3 +description: A Helm chart for Kubernetes +name: parentchart +version: 0.1.0 diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/noreqs/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/noreqs/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/subpop/noreqs/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/subpop/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/subpop/values.yaml new file mode 100644 index 00000000..ba70ed40 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/subpop/values.yaml @@ -0,0 +1,45 @@ +# parent/values.yaml + +imported-chart1: + SPextra1: "helm rocks" + +overridden-chart1: + SC1bool: false + SC1float: 3.141592 + SC1int: 99 + SC1string: "pollywog" + SPextra2: 42 + + +imported-chartA: + SPextra3: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 41.3 + SCAint: 808 + SCAstring: "jabberwocky" + SPextra4: true + +imported-chartA-B: + SPextra5: "k8s" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 41.3 + SCAint: 808 + SCAstring: "jabberwocky" + SCBbool: false + SCBfloat: 1.99 + SCBint: 77 + SCBstring: "jango" + SPextra6: 111 + +tags: + front-end: true + back-end: false + +subchart2alias: + enabled: false + +ensurenull: null diff --git a/pkg/helm/pkg/chartutil/testdata/test-values-invalid.schema.json b/pkg/helm/intern/chart/v3/util/testdata/test-values-invalid.schema.json similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/test-values-invalid.schema.json rename to pkg/helm/intern/chart/v3/util/testdata/test-values-invalid.schema.json diff --git a/pkg/helm/pkg/chartutil/testdata/test-values-negative.yaml b/pkg/helm/intern/chart/v3/util/testdata/test-values-negative.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/test-values-negative.yaml rename to pkg/helm/intern/chart/v3/util/testdata/test-values-negative.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/test-values.schema.json b/pkg/helm/intern/chart/v3/util/testdata/test-values.schema.json similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/test-values.schema.json rename to pkg/helm/intern/chart/v3/util/testdata/test-values.schema.json diff --git a/pkg/helm/pkg/chartutil/testdata/test-values.yaml b/pkg/helm/intern/chart/v3/util/testdata/test-values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/test-values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/test-values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/README.md b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/README.md rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/README.md diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml new file mode 100644 index 00000000..1026f890 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml @@ -0,0 +1,19 @@ +apiVersion: v3 +name: umbrella +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: app1 + version: 0.1.0 + condition: app1.enabled +- name: app2 + version: 0.1.0 + condition: app2.enabled +- name: app3 + version: 0.1.0 + condition: app3.enabled +- name: app4 + version: 0.1.0 + condition: app4.enabled diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml new file mode 100644 index 00000000..5bdf2157 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v3 +name: app1 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 + import-values: + - defaults diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml new file mode 100644 index 00000000..9bc30636 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml new file mode 100644 index 00000000..1313ce4e --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v3 +name: app2 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 + import-values: + - defaults diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml new file mode 100644 index 00000000..9bc30636 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml new file mode 100644 index 00000000..1a80533d --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v3 +name: app3 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 + import-values: + - defaults diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml new file mode 100644 index 00000000..9bc30636 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml new file mode 100644 index 00000000..886b4b1e --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v3 +name: app4 +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 + +dependencies: +- name: library + version: 0.1.0 diff --git a/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml new file mode 100644 index 00000000..9bc30636 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v3 +name: library +description: A Helm chart for Kubernetes +type: library +version: 0.1.0 diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml b/pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/values.yaml rename to pkg/helm/intern/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml diff --git a/pkg/helm/intern/chart/v3/util/validate_name.go b/pkg/helm/intern/chart/v3/util/validate_name.go new file mode 100644 index 00000000..6595e085 --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/validate_name.go @@ -0,0 +1,111 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "regexp" +) + +// validName is a regular expression for resource names. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + +var ( + // errMissingName indicates that a release (name) was not provided. + errMissingName = errors.New("no name provided") + + // errInvalidName indicates that an invalid release name was provided + errInvalidName = fmt.Errorf( + "invalid release name, must match regex %s and the length must not be longer than 53", + validName.String()) + + // errInvalidKubernetesName indicates that the name does not meet the Kubernetes + // restrictions on metadata names. + errInvalidKubernetesName = fmt.Errorf( + "invalid metadata name, must match regex %s and the length must not be longer than 253", + validName.String()) +) + +const ( + // According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names) + // some resource names have a max length of 63 characters while others have a max + // length of 253 characters. As we cannot be sure the resources used in a chart, we + // therefore need to limit it to 63 chars and reserve 10 chars for additional part to name + // of the resource. The reason is that chart maintainers can use release name as part of + // the resource name (and some additional chars). + maxReleaseNameLen = 53 + // maxMetadataNameLen is the maximum length Kubernetes allows for any name. + maxMetadataNameLen = 253 +) + +// ValidateReleaseName performs checks for an entry for a Helm release name +// +// For Helm to allow a name, it must be below a certain character count (53) and also match +// a regular expression. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +func ValidateReleaseName(name string) error { + // This case is preserved for backwards compatibility + if name == "" { + return errMissingName + + } + if len(name) > maxReleaseNameLen || !validName.MatchString(name) { + return errInvalidName + } + return nil +} + +// ValidateMetadataName validates the name field of a Kubernetes metadata object. +// +// Empty strings, strings longer than 253 chars, or strings that don't match the regexp +// will fail. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// Deprecated: remove in Helm 4. Name validation now uses rules defined in +// pkg/lint/rules.validateMetadataNameFunc() +func ValidateMetadataName(name string) error { + if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) { + return errInvalidKubernetesName + } + return nil +} diff --git a/pkg/helm/intern/chart/v3/util/validate_name_test.go b/pkg/helm/intern/chart/v3/util/validate_name_test.go new file mode 100644 index 00000000..cfc62a0f --- /dev/null +++ b/pkg/helm/intern/chart/v3/util/validate_name_test.go @@ -0,0 +1,91 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import "testing" + +// TestValidateReleaseName is a regression test for ValidateName +// +// Kubernetes has strict naming conventions for resource names. This test represents +// those conventions. +// +// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// NOTE: At the time of this writing, the docs above say that names cannot begin with +// digits. However, `kubectl`'s regular expression explicit allows this, and +// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits. +func TestValidateReleaseName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + "a1111111111111111111111111111111111111111111111111111111111z": false, + } + for input, expectPass := range names { + if err := ValidateReleaseName(input); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} + +func TestValidateMetadataName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + "a1111111111111111111111111111111111111111111111111111111111z": true, + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z": false, + } + for input, expectPass := range names { + if err := ValidateMetadataName(input); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} diff --git a/pkg/helm/intern/cli/output/color.go b/pkg/helm/intern/cli/output/color.go new file mode 100644 index 00000000..8d3ea084 --- /dev/null +++ b/pkg/helm/intern/cli/output/color.go @@ -0,0 +1,67 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package output + +import ( + "github.com/fatih/color" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" +) + +// ColorizeStatus returns a colorized version of the status string based on the status value +func ColorizeStatus(status common.Status, noColor bool) string { + // Disable color if requested + if noColor { + return status.String() + } + + switch status { + case common.StatusDeployed: + return color.GreenString(status.String()) + case common.StatusFailed: + return color.RedString(status.String()) + case common.StatusPendingInstall, common.StatusPendingUpgrade, common.StatusPendingRollback, common.StatusUninstalling: + return color.YellowString(status.String()) + case common.StatusUnknown: + return color.RedString(status.String()) + default: + // For uninstalled, superseded, and any other status + return status.String() + } +} + +// ColorizeHeader returns a colorized version of a header string +func ColorizeHeader(header string, noColor bool) string { + // Disable color if requested + if noColor { + return header + } + + // Use bold for headers + return color.New(color.Bold).Sprint(header) +} + +// ColorizeNamespace returns a colorized version of a namespace string +func ColorizeNamespace(namespace string, noColor bool) string { + // Disable color if requested + if noColor { + return namespace + } + + // Use cyan for namespaces + return color.CyanString(namespace) +} diff --git a/pkg/helm/intern/cli/output/color_test.go b/pkg/helm/intern/cli/output/color_test.go new file mode 100644 index 00000000..012550fb --- /dev/null +++ b/pkg/helm/intern/cli/output/color_test.go @@ -0,0 +1,172 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package output + +import ( + "strings" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" +) + +func TestColorizeStatus(t *testing.T) { + + tests := []struct { + name string + status common.Status + noColor bool + envNoColor string + wantColor bool // whether we expect color codes in output + }{ + { + name: "deployed status with color", + status: common.StatusDeployed, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "deployed status without color flag", + status: common.StatusDeployed, + noColor: true, + envNoColor: "", + wantColor: false, + }, + { + name: "failed status with color", + status: common.StatusFailed, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "pending install status with color", + status: common.StatusPendingInstall, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "unknown status with color", + status: common.StatusUnknown, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "superseded status with color", + status: common.StatusSuperseded, + noColor: false, + envNoColor: "", + wantColor: false, // superseded doesn't get colored + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("NO_COLOR", tt.envNoColor) + + result := ColorizeStatus(tt.status, tt.noColor) + + // Check if result contains ANSI escape codes + hasColor := strings.Contains(result, "\033[") + + // In test environment, term.IsTerminal will be false, so we won't get color + // unless we're testing the logic without terminal detection + if hasColor && !tt.wantColor { + t.Errorf("ColorizeStatus() returned color when none expected: %q", result) + } + + // Always check the status text is present + if !strings.Contains(result, tt.status.String()) { + t.Errorf("ColorizeStatus() = %q, want to contain %q", result, tt.status.String()) + } + }) + } +} + +func TestColorizeHeader(t *testing.T) { + + tests := []struct { + name string + header string + noColor bool + envNoColor string + }{ + { + name: "header with color", + header: "NAME", + noColor: false, + envNoColor: "", + }, + { + name: "header without color flag", + header: "NAME", + noColor: true, + envNoColor: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("NO_COLOR", tt.envNoColor) + + result := ColorizeHeader(tt.header, tt.noColor) + + // Always check the header text is present + if !strings.Contains(result, tt.header) { + t.Errorf("ColorizeHeader() = %q, want to contain %q", result, tt.header) + } + }) + } +} + +func TestColorizeNamespace(t *testing.T) { + + tests := []struct { + name string + namespace string + noColor bool + envNoColor string + }{ + { + name: "namespace with color", + namespace: "default", + noColor: false, + envNoColor: "", + }, + { + name: "namespace without color flag", + namespace: "default", + noColor: true, + envNoColor: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("NO_COLOR", tt.envNoColor) + + result := ColorizeNamespace(tt.namespace, tt.noColor) + + // Always check the namespace text is present + if !strings.Contains(result, tt.namespace) { + t.Errorf("ColorizeNamespace() = %q, want to contain %q", result, tt.namespace) + } + }) + } +} diff --git a/pkg/helm/intern/copystructure/copystructure.go b/pkg/helm/intern/copystructure/copystructure.go new file mode 100644 index 00000000..c55897aa --- /dev/null +++ b/pkg/helm/intern/copystructure/copystructure.go @@ -0,0 +1,128 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package copystructure + +import ( + "fmt" + "reflect" +) + +// Copy performs a deep copy of the given src. +// This implementation handles the specific use cases needed by Helm. +func Copy(src any) (any, error) { + if src == nil { + return make(map[string]any), nil + } + return copyValue(reflect.ValueOf(src)) +} + +// copyValue handles copying using reflection for non-map types +func copyValue(original reflect.Value) (any, error) { + switch original.Kind() { + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, + reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, + reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64, + reflect.Complex64, reflect.Complex128, reflect.String, reflect.Array: + return original.Interface(), nil + + case reflect.Interface: + if original.IsNil() { + return original.Interface(), nil + } + return copyValue(original.Elem()) + + case reflect.Map: + if original.IsNil() { + return original.Interface(), nil + } + copied := reflect.MakeMap(original.Type()) + + var err error + var child any + iter := original.MapRange() + for iter.Next() { + key := iter.Key() + value := iter.Value() + + if value.Kind() == reflect.Interface && value.IsNil() { + copied.SetMapIndex(key, value) + continue + } + + child, err = copyValue(value) + if err != nil { + return nil, err + } + copied.SetMapIndex(key, reflect.ValueOf(child)) + } + return copied.Interface(), nil + + case reflect.Pointer: + if original.IsNil() { + return original.Interface(), nil + } + copied, err := copyValue(original.Elem()) + if err != nil { + return nil, err + } + ptr := reflect.New(original.Type().Elem()) + ptr.Elem().Set(reflect.ValueOf(copied)) + return ptr.Interface(), nil + + case reflect.Slice: + if original.IsNil() { + return original.Interface(), nil + } + copied := reflect.MakeSlice(original.Type(), original.Len(), original.Cap()) + for i := 0; i < original.Len(); i++ { + elem := original.Index(i) + + // Handle nil values in slices (e.g., interface{} elements that are nil) + if elem.Kind() == reflect.Interface && elem.IsNil() { + copied.Index(i).Set(elem) + continue + } + + val, err := copyValue(elem) + if err != nil { + return nil, err + } + copied.Index(i).Set(reflect.ValueOf(val)) + } + return copied.Interface(), nil + + case reflect.Struct: + copied := reflect.New(original.Type()).Elem() + for i := 0; i < original.NumField(); i++ { + elem, err := copyValue(original.Field(i)) + if err != nil { + return nil, err + } + copied.Field(i).Set(reflect.ValueOf(elem)) + } + return copied.Interface(), nil + + case reflect.Func, reflect.Chan, reflect.UnsafePointer: + if original.IsNil() { + return original.Interface(), nil + } + return original.Interface(), nil + + default: + return original.Interface(), fmt.Errorf("unsupported type %v", original) + } +} diff --git a/pkg/helm/intern/copystructure/copystructure_test.go b/pkg/helm/intern/copystructure/copystructure_test.go new file mode 100644 index 00000000..b21af646 --- /dev/null +++ b/pkg/helm/intern/copystructure/copystructure_test.go @@ -0,0 +1,389 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package copystructure + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCopy_Nil(t *testing.T) { + result, err := Copy(nil) + require.NoError(t, err) + assert.Equal(t, map[string]any{}, result) +} + +func TestCopy_PrimitiveTypes(t *testing.T) { + tests := []struct { + name string + input any + }{ + {"bool", true}, + {"int", 42}, + {"int8", int8(8)}, + {"int16", int16(16)}, + {"int32", int32(32)}, + {"int64", int64(64)}, + {"uint", uint(42)}, + {"uint8", uint8(8)}, + {"uint16", uint16(16)}, + {"uint32", uint32(32)}, + {"uint64", uint64(64)}, + {"float32", float32(3.14)}, + {"float64", 3.14159}, + {"complex64", complex64(1 + 2i)}, + {"complex128", 1 + 2i}, + {"string", "hello world"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Copy(tt.input) + require.NoError(t, err) + assert.Equal(t, tt.input, result) + }) + } +} + +func TestCopy_Array(t *testing.T) { + input := [3]int{1, 2, 3} + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) +} + +func TestCopy_Slice(t *testing.T) { + t.Run("slice of ints", func(t *testing.T) { + input := []int{1, 2, 3, 4, 5} + result, err := Copy(input) + require.NoError(t, err) + + resultSlice, ok := result.([]int) + require.True(t, ok) + assert.Equal(t, input, resultSlice) + + // Verify it's a deep copy by modifying original + input[0] = 999 + assert.Equal(t, 1, resultSlice[0]) + }) + + t.Run("slice of strings", func(t *testing.T) { + input := []string{"a", "b", "c"} + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) + + t.Run("nil slice", func(t *testing.T) { + var input []int + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("slice of maps", func(t *testing.T) { + input := []map[string]any{ + {"key1": "value1"}, + {"key2": "value2"}, + } + result, err := Copy(input) + require.NoError(t, err) + + resultSlice, ok := result.([]map[string]any) + require.True(t, ok) + assert.Equal(t, input, resultSlice) + + // Verify deep copy + input[0]["key1"] = "modified" + assert.Equal(t, "value1", resultSlice[0]["key1"]) + }) + + t.Run("slice with nil elements", func(t *testing.T) { + input := []any{ + "value1", + nil, + "value2", + } + result, err := Copy(input) + require.NoError(t, err) + + resultSlice, ok := result.([]any) + require.True(t, ok) + assert.Equal(t, input, resultSlice) + assert.Nil(t, resultSlice[1]) + }) +} + +func TestCopy_Map(t *testing.T) { + t.Run("map[string]any", func(t *testing.T) { + input := map[string]any{ + "string": "value", + "int": 42, + "bool": true, + "nested": map[string]any{ + "inner": "value", + }, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultMap, ok := result.(map[string]any) + require.True(t, ok) + assert.Equal(t, input, resultMap) + + // Verify deep copy + input["string"] = "modified" + assert.Equal(t, "value", resultMap["string"]) + + nestedInput := input["nested"].(map[string]any) + nestedResult := resultMap["nested"].(map[string]any) + nestedInput["inner"] = "modified" + assert.Equal(t, "value", nestedResult["inner"]) + }) + + t.Run("map[string]string", func(t *testing.T) { + input := map[string]string{ + "key1": "value1", + "key2": "value2", + } + + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) + + t.Run("nil map", func(t *testing.T) { + var input map[string]any + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("map with nil values", func(t *testing.T) { + input := map[string]any{ + "key1": "value1", + "key2": nil, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultMap, ok := result.(map[string]any) + require.True(t, ok) + assert.Equal(t, input, resultMap) + assert.Nil(t, resultMap["key2"]) + }) +} + +func TestCopy_Struct(t *testing.T) { + type TestStruct struct { + Name string + Age int + Active bool + Scores []int + Metadata map[string]any + } + + input := TestStruct{ + Name: "John", + Age: 30, + Active: true, + Scores: []int{95, 87, 92}, + Metadata: map[string]any{ + "level": "advanced", + "tags": []string{"go", "programming"}, + }, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultStruct, ok := result.(TestStruct) + require.True(t, ok) + assert.Equal(t, input, resultStruct) + + // Verify deep copy + input.Name = "Modified" + input.Scores[0] = 999 + assert.Equal(t, "John", resultStruct.Name) + assert.Equal(t, 95, resultStruct.Scores[0]) +} + +func TestCopy_Pointer(t *testing.T) { + t.Run("pointer to int", func(t *testing.T) { + value := 42 + input := &value + + result, err := Copy(input) + require.NoError(t, err) + + resultPtr, ok := result.(*int) + require.True(t, ok) + assert.Equal(t, *input, *resultPtr) + + // Verify they point to different memory locations + assert.NotSame(t, input, resultPtr) + + // Verify deep copy + *input = 999 + assert.Equal(t, 42, *resultPtr) + }) + + t.Run("pointer to struct", func(t *testing.T) { + type Person struct { + Name string + Age int + } + + input := &Person{Name: "Alice", Age: 25} + + result, err := Copy(input) + require.NoError(t, err) + + resultPtr, ok := result.(*Person) + require.True(t, ok) + assert.Equal(t, *input, *resultPtr) + assert.NotSame(t, input, resultPtr) + }) + + t.Run("nil pointer", func(t *testing.T) { + var input *int + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) +} + +func TestCopy_Interface(t *testing.T) { + t.Run("any with value", func(t *testing.T) { + var input any = "hello" + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) + + t.Run("nil any", func(t *testing.T) { + var input any + result, err := Copy(input) + require.NoError(t, err) + // Copy(nil) returns an empty map according to the implementation + assert.Equal(t, map[string]any{}, result) + }) + + t.Run("any with complex value", func(t *testing.T) { + var input any = map[string]any{ + "key": "value", + "nested": map[string]any{ + "inner": 42, + }, + } + + result, err := Copy(input) + require.NoError(t, err) + assert.Equal(t, input, result) + }) +} + +func TestCopy_ComplexNested(t *testing.T) { + input := map[string]any{ + "users": []map[string]any{ + { + "name": "Alice", + "age": 30, + "addresses": []map[string]any{ + {"type": "home", "city": "NYC"}, + {"type": "work", "city": "SF"}, + }, + }, + { + "name": "Bob", + "age": 25, + "addresses": []map[string]any{ + {"type": "home", "city": "LA"}, + }, + }, + }, + "metadata": map[string]any{ + "version": "1.0", + "flags": []bool{true, false, true}, + }, + } + + result, err := Copy(input) + require.NoError(t, err) + + resultMap, ok := result.(map[string]any) + require.True(t, ok) + assert.Equal(t, input, resultMap) + + // Verify deep copy by modifying nested values + users := input["users"].([]map[string]any) + addresses := users[0]["addresses"].([]map[string]any) + addresses[0]["city"] = "Modified" + + resultUsers := resultMap["users"].([]map[string]any) + resultAddresses := resultUsers[0]["addresses"].([]map[string]any) + assert.Equal(t, "NYC", resultAddresses[0]["city"]) +} + +func TestCopy_Functions(t *testing.T) { + t.Run("function", func(t *testing.T) { + input := func() string { return "hello" } + result, err := Copy(input) + require.NoError(t, err) + + // Functions should be copied as-is (same reference) + resultFunc, ok := result.(func() string) + require.True(t, ok) + assert.Equal(t, input(), resultFunc()) + }) + + t.Run("nil function", func(t *testing.T) { + var input func() + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) +} + +func TestCopy_Channels(t *testing.T) { + t.Run("channel", func(t *testing.T) { + input := make(chan int, 1) + input <- 42 + + result, err := Copy(input) + require.NoError(t, err) + + // Channels should be copied as-is (same reference) + resultChan, ok := result.(chan int) + require.True(t, ok) + + // Since channels are copied as references, verify we can read from the result channel + value := <-resultChan + assert.Equal(t, 42, value) + }) + + t.Run("nil channel", func(t *testing.T) { + var input chan int + result, err := Copy(input) + require.NoError(t, err) + assert.Nil(t, result) + }) +} diff --git a/pkg/helm/intern/fileutil/fileutil_test.go b/pkg/helm/intern/fileutil/fileutil_test.go index 92920d3c..71fcae17 100644 --- a/pkg/helm/intern/fileutil/fileutil_test.go +++ b/pkg/helm/intern/fileutil/fileutil_test.go @@ -20,9 +20,12 @@ import ( "bytes" "os" "path/filepath" + "strings" "testing" ) +// TestAtomicWriteFile tests the happy path of AtomicWriteFile function. +// It verifies that the function correctly writes content to a file with the specified mode. func TestAtomicWriteFile(t *testing.T) { dir := t.TempDir() @@ -55,3 +58,90 @@ func TestAtomicWriteFile(t *testing.T) { mode, gotinfo.Mode()) } } + +// TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails +func TestAtomicWriteFile_CreateTempError(t *testing.T) { + invalidPath := "/invalid/path/that/does/not/exist/testfile" + + reader := bytes.NewReader([]byte("test content")) + mode := os.FileMode(0644) + + err := AtomicWriteFile(invalidPath, reader, mode) + if err == nil { + t.Error("Expected error when CreateTemp fails, but got nil") + } +} + +// TestAtomicWriteFile_EmptyContent tests with empty content +func TestAtomicWriteFile_EmptyContent(t *testing.T) { + dir := t.TempDir() + testpath := filepath.Join(dir, "empty_helm") + + reader := bytes.NewReader([]byte("")) + mode := os.FileMode(0644) + + err := AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error with empty content: %s", err) + } + + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if len(got) != 0 { + t.Fatalf("expected empty content, got: %s", string(got)) + } +} + +// TestAtomicWriteFile_LargeContent tests with large content +func TestAtomicWriteFile_LargeContent(t *testing.T) { + dir := t.TempDir() + testpath := filepath.Join(dir, "large_test") + + // Create a large content string + largeContent := strings.Repeat("HELM", 1024*1024) + reader := bytes.NewReader([]byte(largeContent)) + mode := os.FileMode(0644) + + err := AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error with large content: %s", err) + } + + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if largeContent != string(got) { + t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got)) + } +} + +// TestPlatformAtomicWriteFile_OverwritesExisting verifies that the platform +// helper replaces existing files instead of silently skipping them. +func TestPlatformAtomicWriteFile_OverwritesExisting(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "overwrite_test") + + first := bytes.NewReader([]byte("first")) + if err := PlatformAtomicWriteFile(path, first, 0644); err != nil { + t.Fatalf("first write failed: %v", err) + } + + second := bytes.NewReader([]byte("second")) + if err := PlatformAtomicWriteFile(path, second, 0644); err != nil { + t.Fatalf("second write failed: %v", err) + } + + contents, err := os.ReadFile(path) + if err != nil { + t.Fatalf("failed reading result: %v", err) + } + + if string(contents) != "second" { + t.Fatalf("expected file to be overwritten, got %q", string(contents)) + } +} diff --git a/pkg/helm/intern/fileutil/fileutil_unix.go b/pkg/helm/intern/fileutil/fileutil_unix.go new file mode 100644 index 00000000..bbacb10b --- /dev/null +++ b/pkg/helm/intern/fileutil/fileutil_unix.go @@ -0,0 +1,32 @@ +//go:build !windows + +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fileutil + +import ( + "io" + "os" +) + +// PlatformAtomicWriteFile atomically writes a file to disk. +// +// On non-Windows platforms we don't need extra coordination, so this simply +// delegates to AtomicWriteFile to preserve the existing overwrite behaviour. +func PlatformAtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error { + return AtomicWriteFile(filename, reader, mode) +} diff --git a/pkg/helm/intern/fileutil/fileutil_windows.go b/pkg/helm/intern/fileutil/fileutil_windows.go new file mode 100644 index 00000000..17923786 --- /dev/null +++ b/pkg/helm/intern/fileutil/fileutil_windows.go @@ -0,0 +1,54 @@ +//go:build windows + +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package fileutil + +import ( + "io" + "os" + + "github.com/gofrs/flock" +) + +// PlatformAtomicWriteFile atomically writes a file to disk with file locking to +// prevent concurrent writes. This is particularly useful on Windows where +// concurrent writes to the same file can cause "Access Denied" errors. +// +// The function acquires a lock on the target file and performs an atomic write, +// preserving the existing behaviour of overwriting any previous content once +// the lock is obtained. +func PlatformAtomicWriteFile(filename string, reader io.Reader, mode os.FileMode) error { + // Use a separate lock file to coordinate access between processes + // We cannot lock the target file directly as it would prevent the atomic rename + lockFileName := filename + ".lock" + fileLock := flock.New(lockFileName) + + // Lock() ensures serialized access - if another process is writing, this will wait + if err := fileLock.Lock(); err != nil { + return err + } + defer func() { + fileLock.Unlock() + // Clean up the lock file + // Ignore errors as the file might not exist or be in use by another process + os.Remove(lockFileName) + }() + + // Perform the atomic write while holding the lock + return AtomicWriteFile(filename, reader, mode) +} diff --git a/pkg/helm/intern/logging/logging.go b/pkg/helm/intern/logging/logging.go new file mode 100644 index 00000000..674e2db3 --- /dev/null +++ b/pkg/helm/intern/logging/logging.go @@ -0,0 +1,125 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logging + +import ( + "context" + "log/slog" + "os" + "sync/atomic" +) + +// DebugEnabledFunc is a function type that determines if debug logging is enabled +// We use a function because we want to check the setting at log time, not when the logger is created +type DebugEnabledFunc func() bool + +// DebugCheckHandler checks settings.Debug at log time +type DebugCheckHandler struct { + handler slog.Handler + debugEnabled DebugEnabledFunc +} + +// Enabled implements slog.Handler.Enabled +func (h *DebugCheckHandler) Enabled(_ context.Context, level slog.Level) bool { + if level == slog.LevelDebug { + if h.debugEnabled == nil { + return false + } + return h.debugEnabled() + } + return true // Always log other levels +} + +// Handle implements slog.Handler.Handle +func (h *DebugCheckHandler) Handle(ctx context.Context, r slog.Record) error { + return h.handler.Handle(ctx, r) +} + +// WithAttrs implements slog.Handler.WithAttrs +func (h *DebugCheckHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &DebugCheckHandler{ + handler: h.handler.WithAttrs(attrs), + debugEnabled: h.debugEnabled, + } +} + +// WithGroup implements slog.Handler.WithGroup +func (h *DebugCheckHandler) WithGroup(name string) slog.Handler { + return &DebugCheckHandler{ + handler: h.handler.WithGroup(name), + debugEnabled: h.debugEnabled, + } +} + +// NewLogger creates a new logger with dynamic debug checking +func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger { + // Create base handler that removes timestamps + baseHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + // Always use LevelDebug here to allow all messages through + // Our custom handler will do the filtering + Level: slog.LevelDebug, + ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr { + // Remove the time attribute + if a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + }) + + // Wrap with our dynamic debug-checking handler + dynamicHandler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + return slog.New(dynamicHandler) +} + +// LoggerSetterGetter is an interface that can set and get a logger +type LoggerSetterGetter interface { + // SetLogger sets a new slog.Handler + SetLogger(newHandler slog.Handler) + // Logger returns the slog.Logger created from the slog.Handler + Logger() *slog.Logger +} + +type LogHolder struct { + // logger is an atomic.Pointer[slog.Logger] to store the slog.Logger + // We use atomic.Pointer for thread safety + logger atomic.Pointer[slog.Logger] +} + +// Logger returns the logger for the LogHolder. If nil, returns slog.Default(). +func (l *LogHolder) Logger() *slog.Logger { + if lg := l.logger.Load(); lg != nil { + return lg + } + return slog.New(slog.DiscardHandler) // Should never be reached +} + +// SetLogger sets the logger for the LogHolder. If nil, sets the default logger. +func (l *LogHolder) SetLogger(newHandler slog.Handler) { + if newHandler == nil { + l.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs + return + } + l.logger.Store(slog.New(newHandler)) +} + +// Ensure LogHolder implements LoggerSetterGetter +var _ LoggerSetterGetter = &LogHolder{} diff --git a/pkg/helm/intern/logging/logging_test.go b/pkg/helm/intern/logging/logging_test.go new file mode 100644 index 00000000..d22a47a3 --- /dev/null +++ b/pkg/helm/intern/logging/logging_test.go @@ -0,0 +1,373 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logging + +import ( + "bytes" + "context" + "log/slog" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestLogHolder_Logger(t *testing.T) { + t.Run("should return new logger with a then set handler", func(t *testing.T) { + holder := &LogHolder{} + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + + holder.SetLogger(handler) + logger := holder.Logger() + + assert.NotNil(t, logger) + + // Test that the logger works + logger.Info("test message") + assert.Contains(t, buf.String(), "test message") + }) + + t.Run("should return discard - defaultlogger when no handler is set", func(t *testing.T) { + holder := &LogHolder{} + logger := holder.Logger() + + assert.Equal(t, slog.Handler(slog.DiscardHandler), logger.Handler()) + }) +} + +func TestLogHolder_SetLogger(t *testing.T) { + t.Run("sets logger with valid handler", func(t *testing.T) { + holder := &LogHolder{} + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + + holder.SetLogger(handler) + logger := holder.Logger() + + assert.NotNil(t, logger) + + // Compare the handler directly + assert.Equal(t, handler, logger.Handler()) + }) + + t.Run("sets discard logger with nil handler", func(t *testing.T) { + holder := &LogHolder{} + + holder.SetLogger(nil) + logger := holder.Logger() + + assert.NotNil(t, logger) + + assert.Equal(t, slog.Handler(slog.DiscardHandler), logger.Handler()) + }) + + t.Run("can replace existing logger", func(t *testing.T) { + holder := &LogHolder{} + + // Set first logger + buf1 := &bytes.Buffer{} + handler1 := slog.NewTextHandler(buf1, &slog.HandlerOptions{Level: slog.LevelDebug}) + holder.SetLogger(handler1) + + logger1 := holder.Logger() + assert.Equal(t, handler1, logger1.Handler()) + + // Replace with second logger + buf2 := &bytes.Buffer{} + handler2 := slog.NewTextHandler(buf2, &slog.HandlerOptions{Level: slog.LevelDebug}) + holder.SetLogger(handler2) + + logger2 := holder.Logger() + assert.Equal(t, handler2, logger2.Handler()) + }) +} + +func TestLogHolder_InterfaceCompliance(t *testing.T) { + t.Run("implements LoggerSetterGetter interface", func(_ *testing.T) { + var _ LoggerSetterGetter = &LogHolder{} + }) + + t.Run("interface methods work correctly", func(t *testing.T) { + var holder LoggerSetterGetter = &LogHolder{} + + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + + holder.SetLogger(handler) + logger := holder.Logger() + + assert.NotNil(t, logger) + assert.Equal(t, handler, logger.Handler()) + }) +} + +func TestDebugCheckHandler_Enabled(t *testing.T) { + t.Run("returns debugEnabled function result for debug level", func(t *testing.T) { + // Test with debug enabled + debugEnabled := func() bool { return true } + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + assert.True(t, handler.Enabled(t.Context(), slog.LevelDebug)) + }) + + t.Run("returns false for debug level when debug disabled", func(t *testing.T) { + // Test with debug disabled + debugEnabled := func() bool { return false } + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + assert.False(t, handler.Enabled(t.Context(), slog.LevelDebug)) + }) + + t.Run("always returns true for non-debug levels", func(t *testing.T) { + debugEnabled := func() bool { return false } // Debug disabled + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + // Even with debug disabled, other levels should always be enabled + assert.True(t, handler.Enabled(t.Context(), slog.LevelInfo)) + assert.True(t, handler.Enabled(t.Context(), slog.LevelWarn)) + assert.True(t, handler.Enabled(t.Context(), slog.LevelError)) + }) + + t.Run("calls debugEnabled function dynamically", func(t *testing.T) { + callCount := 0 + debugEnabled := func() bool { + callCount++ + return callCount%2 == 1 // Alternates between true and false + } + + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + // First call should return true + assert.True(t, handler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 1, callCount) + + // Second call should return false + assert.False(t, handler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 2, callCount) + + // Third call should return true again + assert.True(t, handler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 3, callCount) + }) +} + +func TestDebugCheckHandler_Handle(t *testing.T) { + t.Run("delegates to underlying handler", func(t *testing.T) { + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: func() bool { return true }, + } + + record := slog.NewRecord(time.Now(), slog.LevelInfo, "test message", 0) + err := handler.Handle(t.Context(), record) + + assert.NoError(t, err) + assert.Contains(t, buf.String(), "test message") + }) + + t.Run("handles context correctly", func(t *testing.T) { + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: func() bool { return true }, + } + + type testKey string + ctx := context.WithValue(t.Context(), testKey("test"), "value") + record := slog.NewRecord(time.Now(), slog.LevelInfo, "context test", 0) + err := handler.Handle(ctx, record) + + assert.NoError(t, err) + assert.Contains(t, buf.String(), "context test") + }) +} + +func TestDebugCheckHandler_WithAttrs(t *testing.T) { + t.Run("returns new DebugCheckHandler with attributes", func(t *testing.T) { + logger := NewLogger(func() bool { return true }) + handler := logger.Handler() + newHandler := handler.WithAttrs([]slog.Attr{ + slog.String("key1", "value1"), + slog.Int("key2", 42), + }) + + // Should return a DebugCheckHandler + debugHandler, ok := newHandler.(*DebugCheckHandler) + assert.True(t, ok) + assert.NotNil(t, debugHandler) + + // Should preserve the debugEnabled function + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelDebug)) + + // Should have the attributes applied to the underlying handler + assert.NotEqual(t, handler, debugHandler.handler) + }) + + t.Run("preserves debugEnabled function", func(t *testing.T) { + callCount := 0 + debugEnabled := func() bool { + callCount++ + return callCount%2 == 1 + } + + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + attrs := []slog.Attr{slog.String("test", "value")} + newHandler := handler.WithAttrs(attrs) + + // The new handler should use the same debugEnabled function + assert.True(t, newHandler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 1, callCount) + + assert.False(t, newHandler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 2, callCount) + }) +} + +func TestDebugCheckHandler_WithGroup(t *testing.T) { + t.Run("returns new DebugCheckHandler with group", func(t *testing.T) { + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: func() bool { return true }, + } + + newHandler := handler.WithGroup("testgroup") + + // Should return a DebugCheckHandler + debugHandler, ok := newHandler.(*DebugCheckHandler) + assert.True(t, ok) + assert.NotNil(t, debugHandler) + + // Should preserve the debugEnabled function + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelDebug)) + + // Should have the group applied to the underlying handler + assert.NotEqual(t, handler.handler, debugHandler.handler) + }) + + t.Run("preserves debugEnabled function", func(t *testing.T) { + callCount := 0 + debugEnabled := func() bool { + callCount++ + return callCount%2 == 1 + } + + buf := &bytes.Buffer{} + baseHandler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + handler := &DebugCheckHandler{ + handler: baseHandler, + debugEnabled: debugEnabled, + } + + newHandler := handler.WithGroup("testgroup") + + // The new handler should use the same debugEnabled function + assert.True(t, newHandler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 1, callCount) + + assert.False(t, newHandler.Enabled(t.Context(), slog.LevelDebug)) + assert.Equal(t, 2, callCount) + }) +} + +func TestDebugCheckHandler_Integration(t *testing.T) { + t.Run("works with NewLogger function", func(t *testing.T) { + debugEnabled := func() bool { return true } + logger := NewLogger(debugEnabled) + + assert.NotNil(t, logger) + + // The logger should have a DebugCheckHandler + handler := logger.Handler() + debugHandler, ok := handler.(*DebugCheckHandler) + assert.True(t, ok) + + // Should enable debug when debugEnabled returns true + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelDebug)) + + // Should enable other levels regardless + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelInfo)) + }) + + t.Run("dynamic debug checking works in practice", func(t *testing.T) { + debugState := false + debugEnabled := func() bool { return debugState } + + logger := NewLogger(debugEnabled) + + // Initially debug should be disabled + assert.False(t, logger.Handler().(*DebugCheckHandler).Enabled(t.Context(), slog.LevelDebug)) + + // Enable debug + debugState = true + assert.True(t, logger.Handler().(*DebugCheckHandler).Enabled(t.Context(), slog.LevelDebug)) + + // Disable debug again + debugState = false + assert.False(t, logger.Handler().(*DebugCheckHandler).Enabled(t.Context(), slog.LevelDebug)) + }) + + t.Run("handles nil debugEnabled function", func(t *testing.T) { + logger := NewLogger(nil) + + assert.NotNil(t, logger) + + // The logger should have a DebugCheckHandler + handler := logger.Handler() + debugHandler, ok := handler.(*DebugCheckHandler) + assert.True(t, ok) + + // When debugEnabled is nil, debug level should be disabled (default behavior) + assert.False(t, debugHandler.Enabled(t.Context(), slog.LevelDebug)) + + // Other levels should always be enabled + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelInfo)) + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelWarn)) + assert.True(t, debugHandler.Enabled(t.Context(), slog.LevelError)) + }) +} diff --git a/pkg/helm/intern/monocular/client.go b/pkg/helm/intern/monocular/client.go index 88a2564b..f4ef5d64 100644 --- a/pkg/helm/intern/monocular/client.go +++ b/pkg/helm/intern/monocular/client.go @@ -29,9 +29,6 @@ type Client struct { // The base URL for requests BaseURL string - - // The internal logger to use - Log func(string, ...interface{}) } // New creates a new client @@ -44,12 +41,9 @@ func New(u string) (*Client, error) { return &Client{ BaseURL: u, - Log: nopLogger, }, nil } -var nopLogger = func(_ string, _ ...interface{}) {} - // Validate if the base URL for monocular is valid. func validate(u string) error { diff --git a/pkg/helm/intern/monocular/search.go b/pkg/helm/intern/monocular/search.go index ac229562..e5accbaf 100644 --- a/pkg/helm/intern/monocular/search.go +++ b/pkg/helm/intern/monocular/search.go @@ -25,7 +25,7 @@ import ( "time" "github.com/werf/nelm/pkg/helm/intern/version" - "github.com/werf/nelm/pkg/helm/pkg/chart" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" ) // SearchPath is the url path to the search API in monocular. @@ -129,7 +129,7 @@ func (c *Client) Search(term string) ([]SearchResult, error) { } defer res.Body.Close() - if res.StatusCode != 200 { + if res.StatusCode != http.StatusOK { return nil, fmt.Errorf("failed to fetch %s : %s", p.String(), res.Status) } diff --git a/pkg/helm/intern/monocular/search_test.go b/pkg/helm/intern/monocular/search_test.go index 9f6954af..fc82ef4b 100644 --- a/pkg/helm/intern/monocular/search_test.go +++ b/pkg/helm/intern/monocular/search_test.go @@ -28,7 +28,7 @@ var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attribute func TestSearch(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, searchResult) })) defer ts.Close() diff --git a/pkg/helm/intern/resolver/resolver.go b/pkg/helm/intern/resolver/resolver.go index 3b6611fa..5d29806f 100644 --- a/pkg/helm/intern/resolver/resolver.go +++ b/pkg/helm/intern/resolver/resolver.go @@ -17,23 +17,24 @@ package resolver import ( "bytes" + "context" "encoding/json" + "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" "time" "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/provenance" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) // Resolver resolves dependencies from semantic version ranges to a particular version. @@ -53,7 +54,7 @@ func New(chartpath, cachepath string, registryClient *registry.Client) *Resolver } // Resolve resolves dependencies and returns a lock file with the resolution. -func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string, opts helmopts.HelmOptions) (*chart.Lock, error) { +func (r *Resolver) Resolve(ctx context.Context, reqs []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { // Now we clone the dependencies, locking as we go. locked := make([]*chart.Dependency, len(reqs)) @@ -61,7 +62,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string for i, d := range reqs { constraint, err := semver.NewConstraint(d.Version) if err != nil { - return nil, errors.Wrapf(err, "dependency %q has an invalid version/constraint format", d.Name) + return nil, fmt.Errorf("dependency %q has an invalid version/constraint format: %w", d.Name, err) } if d.Repository == "" { @@ -78,13 +79,12 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string continue } if strings.HasPrefix(d.Repository, "file://") { - chartpath, err := GetLocalPath(d.Repository, r.chartpath) if err != nil { return nil, err } - ch, err := loader.LoadDir(chartpath, opts) + ch, err := loader.LoadDir(ctx, chartpath) if err != nil { return nil, err } @@ -96,7 +96,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string } if !constraint.Check(v) { - missing = append(missing, d.Name) + missing = append(missing, fmt.Sprintf("%q (repository %q, version %q)", d.Name, d.Repository, d.Version)) continue } @@ -126,12 +126,12 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string if !registry.IsOCI(d.Repository) { repoIndex, err := repo.LoadIndexFile(filepath.Join(r.cachepath, helmpath.CacheIndexFile(repoName))) if err != nil { - return nil, errors.Wrapf(err, "no cached repository for %s found. (try 'helm repo update')", repoName) + return nil, fmt.Errorf("no cached repository for %s found. (try 'helm repo update'): %w", repoName, err) } vs, ok = repoIndex.Entries[d.Name] if !ok { - return nil, errors.Errorf("%s chart not found in repo %s", d.Name, d.Repository) + return nil, fmt.Errorf("%s chart not found in repo %s", d.Name, d.Repository) } found = false } else { @@ -153,7 +153,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string ref := fmt.Sprintf("%s/%s", strings.TrimPrefix(d.Repository, fmt.Sprintf("%s://", registry.OCIScheme)), d.Name) tags, err := r.registryClient.Tags(ref) if err != nil { - return nil, errors.Wrapf(err, "could not retrieve list of tags for repository %s", d.Repository) + return nil, fmt.Errorf("could not retrieve list of tags for repository %s: %w", d.Repository, err) } vs = make(repo.ChartVersions, len(tags)) @@ -174,7 +174,7 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string Repository: d.Repository, Version: version, } - // The version are already sorted and hence the first one to satisfy the constraint is used + // The versions are already sorted and hence the first one to satisfy the constraint is used for _, ver := range vs { v, err := semver.NewVersion(ver.Version) // OCI does not need URLs @@ -190,11 +190,11 @@ func (r *Resolver) Resolve(reqs []*chart.Dependency, repoNames map[string]string } if !found { - missing = append(missing, d.Name) + missing = append(missing, fmt.Sprintf("%q (repository %q, version %q)", d.Name, d.Repository, d.Version)) } } if len(missing) > 0 { - return nil, errors.Errorf("can't get a valid version for repositories %s. Try changing the version constraint in Chart.yaml", strings.Join(missing, ", ")) + return nil, fmt.Errorf("can't get a valid version for %d subchart(s): %s. Make sure a matching chart version exists in the repo, or change the version constraint in Chart.yaml", len(missing), strings.Join(missing, ", ")) } digest, err := HashReq(reqs, locked) @@ -254,8 +254,8 @@ func GetLocalPath(repo, chartpath string) (string, error) { depPath = filepath.Join(chartpath, p) } - if _, err = os.Stat(depPath); os.IsNotExist(err) { - return "", errors.Errorf("directory %s not found", depPath) + if _, err = os.Stat(depPath); errors.Is(err, fs.ErrNotExist) { + return "", fmt.Errorf("directory %s not found", depPath) } else if err != nil { return "", err } diff --git a/pkg/helm/intern/resolver/resolver_test.go b/pkg/helm/intern/resolver/resolver_test.go index cfbf16d2..64889853 100644 --- a/pkg/helm/intern/resolver/resolver_test.go +++ b/pkg/helm/intern/resolver/resolver_test.go @@ -16,10 +16,11 @@ limitations under the License. package resolver import ( + "context" "runtime" "testing" - "github.com/werf/nelm/pkg/helm/pkg/chart" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" "github.com/werf/nelm/pkg/helm/pkg/registry" ) @@ -144,7 +145,7 @@ func TestResolve(t *testing.T) { r := New("testdata/chartpath", "testdata/repository", registryClient) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l, err := r.Resolve(tt.req, repoNames) + l, err := r.Resolve(context.Background(), tt.req, repoNames) if err != nil { if tt.err { return diff --git a/pkg/helm/intern/statusreaders/job_status_reader.go b/pkg/helm/intern/statusreaders/job_status_reader.go new file mode 100644 index 00000000..3cd9ac7a --- /dev/null +++ b/pkg/helm/intern/statusreaders/job_status_reader.go @@ -0,0 +1,121 @@ +/* +Copyright The Helm Authors. +This file was initially copied and modified from + https://github.com/fluxcd/kustomize-controller/blob/main/internal/statusreaders/job.go +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statusreaders + +import ( + "context" + "fmt" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders" + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + "github.com/fluxcd/cli-utils/pkg/object" +) + +type customJobStatusReader struct { + genericStatusReader engine.StatusReader +} + +func NewCustomJobStatusReader(mapper meta.RESTMapper) engine.StatusReader { + genericStatusReader := statusreaders.NewGenericStatusReader(mapper, jobConditions) + return &customJobStatusReader{ + genericStatusReader: genericStatusReader, + } +} + +func (j *customJobStatusReader) Supports(gk schema.GroupKind) bool { + return gk == batchv1.SchemeGroupVersion.WithKind("Job").GroupKind() +} + +func (j *customJobStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) { + return j.genericStatusReader.ReadStatus(ctx, reader, resource) +} + +func (j *customJobStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) { + return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource) +} + +// Ref: https://github.com/kubernetes-sigs/cli-utils/blob/v0.29.4/pkg/kstatus/status/core.go +// Modified to return Current status only when the Job has completed as opposed to when it's in progress. +func jobConditions(u *unstructured.Unstructured) (*status.Result, error) { + obj := u.UnstructuredContent() + + parallelism := status.GetIntField(obj, ".spec.parallelism", 1) + completions := status.GetIntField(obj, ".spec.completions", parallelism) + succeeded := status.GetIntField(obj, ".status.succeeded", 0) + failed := status.GetIntField(obj, ".status.failed", 0) + + // Conditions + // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/job/utils.go#L24 + objc, err := status.GetObjectWithConditions(obj) + if err != nil { + return nil, err + } + for _, c := range objc.Status.Conditions { + switch c.Type { + case "Complete": + if c.Status == corev1.ConditionTrue { + message := fmt.Sprintf("Job Completed. succeeded: %d/%d", succeeded, completions) + return &status.Result{ + Status: status.CurrentStatus, + Message: message, + Conditions: []status.Condition{}, + }, nil + } + case "Failed": + message := fmt.Sprintf("Job Failed. failed: %d/%d", failed, completions) + if c.Status == corev1.ConditionTrue { + return &status.Result{ + Status: status.FailedStatus, + Message: message, + Conditions: []status.Condition{ + { + Type: status.ConditionStalled, + Status: corev1.ConditionTrue, + Reason: "JobFailed", + Message: message, + }, + }, + }, nil + } + } + } + + message := "Job in progress" + return &status.Result{ + Status: status.InProgressStatus, + Message: message, + Conditions: []status.Condition{ + { + Type: status.ConditionReconciling, + Status: corev1.ConditionTrue, + Reason: "JobInProgress", + Message: message, + }, + }, + }, nil +} diff --git a/pkg/helm/intern/statusreaders/job_status_reader_test.go b/pkg/helm/intern/statusreaders/job_status_reader_test.go new file mode 100644 index 00000000..6e9ed5a7 --- /dev/null +++ b/pkg/helm/intern/statusreaders/job_status_reader_test.go @@ -0,0 +1,116 @@ +/* +Copyright The Helm Authors. +This file was initially copied and modified from + https://github.com/fluxcd/kustomize-controller/blob/main/internal/statusreaders/job_test.go +Copyright 2022 The Flux authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statusreaders + +import ( + "testing" + + "github.com/stretchr/testify/assert" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/fluxcd/cli-utils/pkg/kstatus/status" +) + +func toUnstructured(t *testing.T, obj runtime.Object) (*unstructured.Unstructured, error) { + t.Helper() + // If the incoming object is already unstructured, perform a deep copy first + // otherwise DefaultUnstructuredConverter ends up returning the inner map without + // making a copy. + if _, ok := obj.(runtime.Unstructured); ok { + obj = obj.DeepCopyObject() + } + rawMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + return &unstructured.Unstructured{Object: rawMap}, nil +} + +func TestJobConditions(t *testing.T) { + t.Parallel() + tests := []struct { + name string + job *batchv1.Job + expectedStatus status.Status + }{ + { + name: "job without Complete condition returns InProgress status", + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job-no-condition", + }, + Spec: batchv1.JobSpec{}, + Status: batchv1.JobStatus{}, + }, + expectedStatus: status.InProgressStatus, + }, + { + name: "job with Complete condition as True returns Current status", + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job-complete", + }, + Spec: batchv1.JobSpec{}, + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + expectedStatus: status.CurrentStatus, + }, + { + name: "job with Failed condition as True returns Failed status", + job: &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: "job-failed", + }, + Spec: batchv1.JobSpec{}, + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobFailed, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + expectedStatus: status.FailedStatus, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + us, err := toUnstructured(t, tc.job) + assert.NoError(t, err) + result, err := jobConditions(us) + assert.NoError(t, err) + assert.Equal(t, tc.expectedStatus, result.Status) + }) + } +} diff --git a/pkg/helm/intern/statusreaders/pod_status_reader.go b/pkg/helm/intern/statusreaders/pod_status_reader.go new file mode 100644 index 00000000..bf633c0d --- /dev/null +++ b/pkg/helm/intern/statusreaders/pod_status_reader.go @@ -0,0 +1,104 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statusreaders + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders" + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + "github.com/fluxcd/cli-utils/pkg/object" +) + +type customPodStatusReader struct { + genericStatusReader engine.StatusReader +} + +func NewCustomPodStatusReader(mapper meta.RESTMapper) engine.StatusReader { + genericStatusReader := statusreaders.NewGenericStatusReader(mapper, podConditions) + return &customPodStatusReader{ + genericStatusReader: genericStatusReader, + } +} + +func (j *customPodStatusReader) Supports(gk schema.GroupKind) bool { + return gk == corev1.SchemeGroupVersion.WithKind("Pod").GroupKind() +} + +func (j *customPodStatusReader) ReadStatus(ctx context.Context, reader engine.ClusterReader, resource object.ObjMetadata) (*event.ResourceStatus, error) { + return j.genericStatusReader.ReadStatus(ctx, reader, resource) +} + +func (j *customPodStatusReader) ReadStatusForObject(ctx context.Context, reader engine.ClusterReader, resource *unstructured.Unstructured) (*event.ResourceStatus, error) { + return j.genericStatusReader.ReadStatusForObject(ctx, reader, resource) +} + +func podConditions(u *unstructured.Unstructured) (*status.Result, error) { + obj := u.UnstructuredContent() + phase := status.GetStringField(obj, ".status.phase", "") + switch corev1.PodPhase(phase) { + case corev1.PodSucceeded: + message := fmt.Sprintf("pod %s succeeded", u.GetName()) + return &status.Result{ + Status: status.CurrentStatus, + Message: message, + Conditions: []status.Condition{ + { + Type: status.ConditionStalled, + Status: corev1.ConditionTrue, + Message: message, + }, + }, + }, nil + case corev1.PodFailed: + message := fmt.Sprintf("pod %s failed", u.GetName()) + return &status.Result{ + Status: status.FailedStatus, + Message: message, + Conditions: []status.Condition{ + { + Type: status.ConditionStalled, + Status: corev1.ConditionTrue, + Reason: "PodFailed", + Message: message, + }, + }, + }, nil + default: + message := "Pod in progress" + return &status.Result{ + Status: status.InProgressStatus, + Message: message, + Conditions: []status.Condition{ + { + Type: status.ConditionReconciling, + Status: corev1.ConditionTrue, + Reason: "PodInProgress", + Message: message, + }, + }, + }, nil + } +} diff --git a/pkg/helm/intern/statusreaders/pod_status_reader_test.go b/pkg/helm/intern/statusreaders/pod_status_reader_test.go new file mode 100644 index 00000000..ba0d1f1b --- /dev/null +++ b/pkg/helm/intern/statusreaders/pod_status_reader_test.go @@ -0,0 +1,111 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package statusreaders + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/fluxcd/cli-utils/pkg/kstatus/status" +) + +func TestPodConditions(t *testing.T) { + tests := []struct { + name string + pod *v1.Pod + expectedStatus status.Status + }{ + { + name: "pod without status returns in progress", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-no-status"}, + Spec: v1.PodSpec{}, + Status: v1.PodStatus{}, + }, + expectedStatus: status.InProgressStatus, + }, + { + name: "pod succeeded returns current status", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-succeeded"}, + Spec: v1.PodSpec{}, + Status: v1.PodStatus{ + Phase: v1.PodSucceeded, + }, + }, + expectedStatus: status.CurrentStatus, + }, + { + name: "pod failed returns failed status", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-failed"}, + Spec: v1.PodSpec{}, + Status: v1.PodStatus{ + Phase: v1.PodFailed, + }, + }, + expectedStatus: status.FailedStatus, + }, + { + name: "pod pending returns in progress status", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-pending"}, + Spec: v1.PodSpec{}, + Status: v1.PodStatus{ + Phase: v1.PodPending, + }, + }, + expectedStatus: status.InProgressStatus, + }, + { + name: "pod running returns in progress status", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-running"}, + Spec: v1.PodSpec{}, + Status: v1.PodStatus{ + Phase: v1.PodRunning, + }, + }, + expectedStatus: status.InProgressStatus, + }, + { + name: "pod with unknown phase returns in progress status", + pod: &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod-unknown"}, + Spec: v1.PodSpec{}, + Status: v1.PodStatus{ + Phase: v1.PodUnknown, + }, + }, + expectedStatus: status.InProgressStatus, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + us, err := toUnstructured(t, tc.pod) + assert.NoError(t, err) + result, err := podConditions(us) + assert.NoError(t, err) + assert.Equal(t, tc.expectedStatus, result.Status) + }) + } +} diff --git a/pkg/helm/intern/sympath/walk.go b/pkg/helm/intern/sympath/walk.go index a276cfef..812bb68c 100644 --- a/pkg/helm/intern/sympath/walk.go +++ b/pkg/helm/intern/sympath/walk.go @@ -21,12 +21,11 @@ limitations under the License. package sympath import ( - "log" + "fmt" + "log/slog" "os" "path/filepath" "sort" - - "github.com/pkg/errors" ) // Walk walks the file tree rooted at root, calling walkFn for each file or directory @@ -69,9 +68,10 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if IsSymlink(info) { resolved, err := filepath.EvalSymlinks(path) if err != nil { - return errors.Wrapf(err, "error evaluating symlink %s", path) + return fmt.Errorf("error evaluating symlink %s: %w", path, err) } - log.Printf("found symbolic link in path: %s resolves to %s. Contents of linked file included and used", path, resolved) + // This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons. + slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved) if info, err = os.Lstat(resolved); err != nil { return err } diff --git a/pkg/helm/intern/sympath/walk_test.go b/pkg/helm/intern/sympath/walk_test.go index 25f73713..1eba8b99 100644 --- a/pkg/helm/intern/sympath/walk_test.go +++ b/pkg/helm/intern/sympath/walk_test.go @@ -76,6 +76,7 @@ func walkTree(n *Node, path string, f func(path string, n *Node)) { } func makeTree(t *testing.T) { + t.Helper() walkTree(tree, tree.name, func(path string, n *Node) { if n.entries == nil { if n.symLinkedTo != "" { @@ -99,6 +100,7 @@ func makeTree(t *testing.T) { } func checkMarks(t *testing.T, report bool) { + t.Helper() walkTree(tree, tree.name, func(path string, n *Node) { if n.marks != n.expectedMarks && report { t.Errorf("node %s mark = %d; expected %d", path, n.marks, n.expectedMarks) @@ -108,18 +110,18 @@ func checkMarks(t *testing.T, report bool) { } // Assumes that each node name is unique. Good enough for a test. -// If clear is true, any incoming error is cleared before return. The errors -// are always accumulated, though. -func mark(info os.FileInfo, err error, errors *[]error, clear bool) error { +// If clearIncomingError is true, any incoming error is cleared before +// return. The errors are always accumulated, though. +func mark(info os.FileInfo, err error, errors *[]error, clearIncomingError bool) error { if err != nil { *errors = append(*errors, err) - if clear { + if clearIncomingError { return nil } return err } name := info.Name() - walkTree(tree, tree.name, func(path string, n *Node) { + walkTree(tree, tree.name, func(_ string, n *Node) { if n.name == name { n.marks++ } @@ -130,9 +132,8 @@ func mark(info os.FileInfo, err error, errors *[]error, clear bool) error { func TestWalk(t *testing.T) { makeTree(t) errors := make([]error, 0, 10) - clear := true - markFn := func(path string, info os.FileInfo, err error) error { - return mark(info, err, &errors, clear) + markFn := func(_ string, info os.FileInfo, err error) error { + return mark(info, err, &errors, true) } // Expect no errors. err := Walk(tree.name, markFn) diff --git a/pkg/helm/intern/test/ensure/ensure.go b/pkg/helm/intern/test/ensure/ensure.go index 52fdb27a..70a2250c 100644 --- a/pkg/helm/intern/test/ensure/ensure.go +++ b/pkg/helm/intern/test/ensure/ensure.go @@ -29,12 +29,12 @@ import ( func HelmHome(t *testing.T) { t.Helper() base := t.TempDir() - os.Setenv(xdg.CacheHomeEnvVar, base) - os.Setenv(xdg.ConfigHomeEnvVar, base) - os.Setenv(xdg.DataHomeEnvVar, base) - os.Setenv(helmpath.CacheHomeEnvVar, "") - os.Setenv(helmpath.ConfigHomeEnvVar, "") - os.Setenv(helmpath.DataHomeEnvVar, "") + t.Setenv(xdg.CacheHomeEnvVar, base) + t.Setenv(xdg.ConfigHomeEnvVar, base) + t.Setenv(xdg.DataHomeEnvVar, base) + t.Setenv(helmpath.CacheHomeEnvVar, "") + t.Setenv(helmpath.ConfigHomeEnvVar, "") + t.Setenv(helmpath.DataHomeEnvVar, "") } // TempFile ensures a temp file for unit testing purposes. @@ -46,9 +46,10 @@ func HelmHome(t *testing.T) { // tempdir := TempFile(t, "foo", []byte("bar")) // filename := filepath.Join(tempdir, "foo") func TempFile(t *testing.T, name string, data []byte) string { + t.Helper() path := t.TempDir() filename := filepath.Join(path, name) - if err := os.WriteFile(filename, data, 0755); err != nil { + if err := os.WriteFile(filename, data, 0o755); err != nil { t.Fatal(err) } return path diff --git a/pkg/helm/intern/test/test.go b/pkg/helm/intern/test/test.go index e6821282..632bc72f 100644 --- a/pkg/helm/intern/test/test.go +++ b/pkg/helm/intern/test/test.go @@ -19,10 +19,9 @@ package test import ( "bytes" "flag" + "fmt" "os" "path/filepath" - - "github.com/pkg/errors" ) // UpdateGolden writes out the golden files with the latest values, rather than failing the test. @@ -75,11 +74,11 @@ func compare(actual []byte, filename string) error { expected, err := os.ReadFile(filename) if err != nil { - return errors.Wrapf(err, "unable to read testdata %s", filename) + return fmt.Errorf("unable to read testdata %s: %w", filename, err) } expected = normalize(expected) if !bytes.Equal(expected, actual) { - return errors.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual) + return fmt.Errorf("does not match golden file %s\n\nWANT:\n'%s'\n\nGOT:\n'%s'", filename, expected, actual) } return nil } @@ -92,5 +91,5 @@ func update(filename string, in []byte) error { } func normalize(in []byte) []byte { - return bytes.Replace(in, []byte("\r\n"), []byte("\n"), -1) + return bytes.ReplaceAll(in, []byte("\r\n"), []byte("\n")) } diff --git a/pkg/helm/intern/third_party/dep/fs/fs.go b/pkg/helm/intern/third_party/dep/fs/fs.go index 4e4eacc6..6e2720f3 100644 --- a/pkg/helm/intern/third_party/dep/fs/fs.go +++ b/pkg/helm/intern/third_party/dep/fs/fs.go @@ -32,13 +32,14 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package fs import ( + "errors" + "fmt" "io" + "io/fs" "os" "path/filepath" "runtime" "syscall" - - "github.com/pkg/errors" ) // fs contains a copy of a few functions from dep tool code to avoid a dependency on golang/dep. @@ -51,7 +52,7 @@ import ( func RenameWithFallback(src, dst string) error { _, err := os.Stat(src) if err != nil { - return errors.Wrapf(err, "cannot stat %s", src) + return fmt.Errorf("cannot stat %s: %w", src, err) } err = os.Rename(src, dst) @@ -69,20 +70,24 @@ func renameByCopy(src, dst string) error { if dir, _ := IsDir(src); dir { cerr = CopyDir(src, dst) if cerr != nil { - cerr = errors.Wrap(cerr, "copying directory failed") + cerr = fmt.Errorf("copying directory failed: %w", cerr) } } else { - cerr = copyFile(src, dst) + cerr = CopyFile(src, dst) if cerr != nil { - cerr = errors.Wrap(cerr, "copying file failed") + cerr = fmt.Errorf("copying file failed: %w", cerr) } } if cerr != nil { - return errors.Wrapf(cerr, "rename fallback failed: cannot rename %s to %s", src, dst) + return fmt.Errorf("rename fallback failed: cannot rename %s to %s: %w", src, dst, cerr) + } + + if err := os.RemoveAll(src); err != nil { + return fmt.Errorf("cannot delete %s: %w", src, err) } - return errors.Wrapf(os.RemoveAll(src), "cannot delete %s", src) + return nil } var ( @@ -107,7 +112,7 @@ func CopyDir(src, dst string) error { } _, err = os.Stat(dst) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } if err == nil { @@ -115,12 +120,12 @@ func CopyDir(src, dst string) error { } if err = os.MkdirAll(dst, fi.Mode()); err != nil { - return errors.Wrapf(err, "cannot mkdir %s", dst) + return fmt.Errorf("cannot mkdir %s: %w", dst, err) } entries, err := os.ReadDir(src) if err != nil { - return errors.Wrapf(err, "cannot read directory %s", dst) + return fmt.Errorf("cannot read directory %s: %w", dst, err) } for _, entry := range entries { @@ -129,13 +134,13 @@ func CopyDir(src, dst string) error { if entry.IsDir() { if err = CopyDir(srcPath, dstPath); err != nil { - return errors.Wrap(err, "copying directory failed") + return fmt.Errorf("copying directory failed: %w", err) } } else { // This will include symlinks, which is what we want when // copying things. - if err = copyFile(srcPath, dstPath); err != nil { - return errors.Wrap(err, "copying file failed") + if err = CopyFile(srcPath, dstPath); err != nil { + return fmt.Errorf("copying file failed: %w", err) } } } @@ -143,13 +148,13 @@ func CopyDir(src, dst string) error { return nil } -// copyFile copies the contents of the file named src to the file named +// CopyFile copies the contents of the file named src to the file named // by dst. The file will be created if it does not already exist. If the // destination file exists, all its contents will be replaced by the contents // of the source file. The file mode will be copied from the source. -func copyFile(src, dst string) (err error) { +func CopyFile(src, dst string) (err error) { if sym, err := IsSymlink(src); err != nil { - return errors.Wrap(err, "symlink check failed") + return fmt.Errorf("symlink check failed: %w", err) } else if sym { if err := cloneSymlink(src, dst); err != nil { if runtime.GOOS == "windows" { @@ -172,28 +177,28 @@ func copyFile(src, dst string) (err error) { in, err := os.Open(src) if err != nil { - return + return err } defer in.Close() out, err := os.Create(dst) if err != nil { - return + return err } if _, err = io.Copy(out, in); err != nil { out.Close() - return + return err } // Check for write errors on Close if err = out.Close(); err != nil { - return + return err } si, err := os.Stat(src) if err != nil { - return + return err } // Temporary fix for Go < 1.9 @@ -205,7 +210,7 @@ func copyFile(src, dst string) (err error) { } err = os.Chmod(dst, si.Mode()) - return + return err } // cloneSymlink will create a new symlink that points to the resolved path of sl. @@ -226,7 +231,7 @@ func IsDir(name string) (bool, error) { return false, err } if !fi.IsDir() { - return false, errors.Errorf("%q is not a directory", name) + return false, fmt.Errorf("%q is not a directory", name) } return true, nil } @@ -260,7 +265,7 @@ func fixLongPath(path string) string { // minus 12)." Since MAX_PATH is 260, 260 - 12 = 248. // // The MSDN docs appear to say that a normal path that is 248 bytes long - // will work; empirically the path must be less then 248 bytes long. + // will work; empirically the path must be less than 248 bytes long. if len(path) < 248 { // Don't fix. (This is how Go 1.7 and earlier worked, // not automatically generating the \\?\ form) diff --git a/pkg/helm/intern/third_party/dep/fs/fs_test.go b/pkg/helm/intern/third_party/dep/fs/fs_test.go index d42c3f11..610771bc 100644 --- a/pkg/helm/intern/third_party/dep/fs/fs_test.go +++ b/pkg/helm/intern/third_party/dep/fs/fs_test.go @@ -33,17 +33,11 @@ package fs import ( "os" - "os/exec" "path/filepath" "runtime" - "sync" "testing" ) -var ( - mu sync.Mutex -) - func TestRenameWithFallback(t *testing.T) { dir := t.TempDir() @@ -332,7 +326,7 @@ func TestCopyFile(t *testing.T) { srcf.Close() destf := filepath.Join(dir, "destf") - if err := copyFile(srcf.Name(), destf); err != nil { + if err := CopyFile(srcf.Name(), destf); err != nil { t.Fatal(err) } @@ -360,19 +354,6 @@ func TestCopyFile(t *testing.T) { } } -func cleanUpDir(dir string) { - // NOTE(mattn): It seems that sometimes git.exe is not dead - // when cleanUpDir() is called. But we do not know any way to wait for it. - if runtime.GOOS == "windows" { - mu.Lock() - exec.Command(`taskkill`, `/F`, `/IM`, `git.exe`).Run() - mu.Unlock() - } - if dir != "" { - os.RemoveAll(dir) - } -} - func TestCopyFileSymlink(t *testing.T) { tempdir := t.TempDir() @@ -385,7 +366,7 @@ func TestCopyFileSymlink(t *testing.T) { for symlink, dst := range testcases { t.Run(symlink, func(t *testing.T) { var err error - if err = copyFile(symlink, dst); err != nil { + if err = CopyFile(symlink, dst); err != nil { t.Fatalf("failed to copy symlink: %s", err) } @@ -457,7 +438,7 @@ func TestCopyFileFail(t *testing.T) { defer cleanup() fn := filepath.Join(dstdir, "file") - if err := copyFile(srcf.Name(), fn); err == nil { + if err := CopyFile(srcf.Name(), fn); err == nil { t.Fatalf("expected error for %s, got none", fn) } } @@ -476,6 +457,7 @@ func TestCopyFileFail(t *testing.T) { // files this function creates. It is the caller's responsibility to call // this function before the test is done running, whether there's an error or not. func setupInaccessibleDir(t *testing.T, op func(dir string) error) func() { + t.Helper() dir := t.TempDir() subdir := filepath.Join(dir, "dir") diff --git a/pkg/helm/intern/third_party/dep/fs/rename.go b/pkg/helm/intern/third_party/dep/fs/rename.go index a3e5e56a..5f13b1ca 100644 --- a/pkg/helm/intern/third_party/dep/fs/rename.go +++ b/pkg/helm/intern/third_party/dep/fs/rename.go @@ -34,10 +34,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package fs import ( + "fmt" "os" "syscall" - - "github.com/pkg/errors" ) // renameFallback attempts to determine the appropriate fallback to failed rename @@ -51,7 +50,7 @@ func renameFallback(err error, src, dst string) error { if !ok { return err } else if terr.Err != syscall.EXDEV { - return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst) + return fmt.Errorf("link error: cannot rename %s to %s: %w", src, dst, terr) } return renameByCopy(src, dst) diff --git a/pkg/helm/intern/third_party/dep/fs/rename_windows.go b/pkg/helm/intern/third_party/dep/fs/rename_windows.go index a377720a..566f695d 100644 --- a/pkg/helm/intern/third_party/dep/fs/rename_windows.go +++ b/pkg/helm/intern/third_party/dep/fs/rename_windows.go @@ -34,10 +34,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package fs import ( + "fmt" "os" "syscall" - - "github.com/pkg/errors" ) // renameFallback attempts to determine the appropriate fallback to failed rename @@ -61,7 +60,7 @@ func renameFallback(err error, src, dst string) error { // 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error. // See https://msdn.microsoft.com/en-us/library/cc231199.aspx if ok && noerr != 0x11 { - return errors.Wrapf(terr, "link error: cannot rename %s to %s", src, dst) + return fmt.Errorf("link error: cannot rename %s to %s: %w", src, dst, terr) } } diff --git a/pkg/helm/intern/tlsutil/cfg.go b/pkg/helm/intern/tlsutil/cfg.go deleted file mode 100644 index 8b9d4329..00000000 --- a/pkg/helm/intern/tlsutil/cfg.go +++ /dev/null @@ -1,58 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tlsutil - -import ( - "crypto/tls" - "crypto/x509" - "os" - - "github.com/pkg/errors" -) - -// Options represents configurable options used to create client and server TLS configurations. -type Options struct { - CaCertFile string - // If either the KeyFile or CertFile is empty, ClientConfig() will not load them. - KeyFile string - CertFile string - // Client-only options - InsecureSkipVerify bool -} - -// ClientConfig returns a TLS configuration for use by a Helm client. -func ClientConfig(opts Options) (cfg *tls.Config, err error) { - var cert *tls.Certificate - var pool *x509.CertPool - - if opts.CertFile != "" || opts.KeyFile != "" { - if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { - if os.IsNotExist(err) { - return nil, errors.Wrapf(err, "could not load x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile) - } - return nil, errors.Wrapf(err, "could not read x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile) - } - } - if !opts.InsecureSkipVerify && opts.CaCertFile != "" { - if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { - return nil, err - } - } - - cfg = &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify, Certificates: []tls.Certificate{*cert}, RootCAs: pool} - return cfg, nil -} diff --git a/pkg/helm/intern/tlsutil/tls.go b/pkg/helm/intern/tlsutil/tls.go index dc832ed8..88f26d47 100644 --- a/pkg/helm/intern/tlsutil/tls.go +++ b/pkg/helm/intern/tlsutil/tls.go @@ -19,60 +19,104 @@ package tlsutil import ( "crypto/tls" "crypto/x509" + "fmt" "os" - "github.com/pkg/errors" + "errors" ) -// NewClientTLS returns tls.Config appropriate for client auth. -func NewClientTLS(certFile, keyFile, caFile string, insecureSkipTLSverify bool) (*tls.Config, error) { - config := tls.Config{ - InsecureSkipVerify: insecureSkipTLSverify, +type TLSConfigOptions struct { + insecureSkipTLSVerify bool + certPEMBlock, keyPEMBlock []byte + caPEMBlock []byte +} + +type TLSConfigOption func(options *TLSConfigOptions) error + +func WithInsecureSkipVerify(insecureSkipTLSVerify bool) TLSConfigOption { + return func(options *TLSConfigOptions) error { + options.insecureSkipTLSVerify = insecureSkipTLSVerify + + return nil } +} + +func WithCertKeyPairFiles(certFile, keyFile string) TLSConfigOption { + return func(options *TLSConfigOptions) error { + if certFile == "" && keyFile == "" { + return nil + } - if certFile != "" && keyFile != "" { - cert, err := CertFromFilePair(certFile, keyFile) + certPEMBlock, err := os.ReadFile(certFile) if err != nil { - return nil, err + return fmt.Errorf("unable to read cert file: %q: %w", certFile, err) } - config.Certificates = []tls.Certificate{*cert} - } - if caFile != "" { - cp, err := CertPoolFromFile(caFile) + keyPEMBlock, err := os.ReadFile(keyFile) if err != nil { - return nil, err + return fmt.Errorf("unable to read key file: %q: %w", keyFile, err) } - config.RootCAs = cp + + options.certPEMBlock = certPEMBlock + options.keyPEMBlock = keyPEMBlock + + return nil } +} - return &config, nil +func WithCAFile(caFile string) TLSConfigOption { + return func(options *TLSConfigOptions) error { + if caFile == "" { + return nil + } + + caPEMBlock, err := os.ReadFile(caFile) + if err != nil { + return fmt.Errorf("can't read CA file: %q: %w", caFile, err) + } + + options.caPEMBlock = caPEMBlock + + return nil + } } -// CertPoolFromFile returns an x509.CertPool containing the certificates -// in the given PEM-encoded file. -// Returns an error if the file could not be read, a certificate could not -// be parsed, or if the file does not contain any certificates -func CertPoolFromFile(filename string) (*x509.CertPool, error) { - b, err := os.ReadFile(filename) - if err != nil { - return nil, errors.Errorf("can't read CA file: %v", filename) +func NewTLSConfig(options ...TLSConfigOption) (*tls.Config, error) { + to := TLSConfigOptions{} + + errs := []error{} + for _, option := range options { + err := option(&to) + if err != nil { + errs = append(errs, err) + } } - cp := x509.NewCertPool() - if !cp.AppendCertsFromPEM(b) { - return nil, errors.Errorf("failed to append certificates from file: %s", filename) + + if len(errs) > 0 { + return nil, errors.Join(errs...) } - return cp, nil -} -// CertFromFilePair returns an tls.Certificate containing the -// certificates public/private key pair from a pair of given PEM-encoded files. -// Returns an error if the file could not be read, a certificate could not -// be parsed, or if the file does not contain any certificates -func CertFromFilePair(certFile, keyFile string) (*tls.Certificate, error) { - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return nil, errors.Wrapf(err, "can't load key pair from cert %s and key %s", certFile, keyFile) + config := tls.Config{ + InsecureSkipVerify: to.insecureSkipTLSVerify, } - return &cert, err + + if len(to.certPEMBlock) > 0 && len(to.keyPEMBlock) > 0 { + cert, err := tls.X509KeyPair(to.certPEMBlock, to.keyPEMBlock) + if err != nil { + return nil, fmt.Errorf("unable to load cert from key pair: %w", err) + } + + config.Certificates = []tls.Certificate{cert} + } + + if len(to.caPEMBlock) > 0 { + cp := x509.NewCertPool() + if !cp.AppendCertsFromPEM(to.caPEMBlock) { + return nil, fmt.Errorf("failed to append certificates from pem block") + } + + config.RootCAs = cp + } + + return &config, nil } diff --git a/pkg/helm/intern/tlsutil/tls_test.go b/pkg/helm/intern/tlsutil/tls_test.go new file mode 100644 index 00000000..f16eb218 --- /dev/null +++ b/pkg/helm/intern/tlsutil/tls_test.go @@ -0,0 +1,106 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tlsutil + +import ( + "path/filepath" + "testing" +) + +const tlsTestDir = "../../testdata" + +const ( + testCaCertFile = "rootca.crt" + testCertFile = "crt.pem" + testKeyFile = "key.pem" +) + +func testfile(t *testing.T, file string) (path string) { + t.Helper() + path, err := filepath.Abs(filepath.Join(tlsTestDir, file)) + if err != nil { + t.Fatalf("error getting absolute path to test file %q: %v", file, err) + } + return path +} + +func TestNewTLSConfig(t *testing.T) { + certFile := testfile(t, testCertFile) + keyFile := testfile(t, testKeyFile) + caCertFile := testfile(t, testCaCertFile) + insecureSkipTLSVerify := false + + { + cfg, err := NewTLSConfig( + WithInsecureSkipVerify(insecureSkipTLSVerify), + WithCertKeyPairFiles(certFile, keyFile), + WithCAFile(caCertFile), + ) + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") + } + } + { + cfg, err := NewTLSConfig( + WithInsecureSkipVerify(insecureSkipTLSVerify), + WithCAFile(caCertFile), + ) + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 0 { + t.Fatalf("expecting 0 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") + } + } + + { + cfg, err := NewTLSConfig( + WithInsecureSkipVerify(insecureSkipTLSVerify), + WithCertKeyPairFiles(certFile, keyFile), + ) + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mismatch, expecting false") + } + if cfg.RootCAs != nil { + t.Fatalf("mismatch tls RootCAs, expecting nil") + } + } +} diff --git a/pkg/helm/intern/tlsutil/tlsutil_test.go b/pkg/helm/intern/tlsutil/tlsutil_test.go deleted file mode 100644 index e31a873d..00000000 --- a/pkg/helm/intern/tlsutil/tlsutil_test.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package tlsutil - -import ( - "path/filepath" - "testing" -) - -const tlsTestDir = "../../testdata" - -const ( - testCaCertFile = "rootca.crt" - testCertFile = "crt.pem" - testKeyFile = "key.pem" -) - -func TestClientConfig(t *testing.T) { - opts := Options{ - CaCertFile: testfile(t, testCaCertFile), - CertFile: testfile(t, testCertFile), - KeyFile: testfile(t, testKeyFile), - InsecureSkipVerify: false, - } - - cfg, err := ClientConfig(opts) - if err != nil { - t.Fatalf("error building tls client config: %v", err) - } - - if got := len(cfg.Certificates); got != 1 { - t.Fatalf("expecting 1 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatalf("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs == nil { - t.Fatalf("mismatch tls RootCAs, expecting non-nil") - } -} - -func testfile(t *testing.T, file string) (path string) { - var err error - if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil { - t.Fatalf("error getting absolute path to test file %q: %v", file, err) - } - return path -} - -func TestNewClientTLS(t *testing.T) { - certFile := testfile(t, testCertFile) - keyFile := testfile(t, testKeyFile) - caCertFile := testfile(t, testCaCertFile) - insecureSkipTLSverify := false - - cfg, err := NewClientTLS(certFile, keyFile, caCertFile, insecureSkipTLSverify) - if err != nil { - t.Error(err) - } - - if got := len(cfg.Certificates); got != 1 { - t.Fatalf("expecting 1 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatalf("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs == nil { - t.Fatalf("mismatch tls RootCAs, expecting non-nil") - } - - cfg, err = NewClientTLS("", "", caCertFile, insecureSkipTLSverify) - if err != nil { - t.Error(err) - } - - if got := len(cfg.Certificates); got != 0 { - t.Fatalf("expecting 0 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatalf("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs == nil { - t.Fatalf("mismatch tls RootCAs, expecting non-nil") - } - - cfg, err = NewClientTLS(certFile, keyFile, "", insecureSkipTLSverify) - if err != nil { - t.Error(err) - } - - if got := len(cfg.Certificates); got != 1 { - t.Fatalf("expecting 1 client certificates, got %d", got) - } - if cfg.InsecureSkipVerify { - t.Fatalf("insecure skip verify mismatch, expecting false") - } - if cfg.RootCAs != nil { - t.Fatalf("mismatch tls RootCAs, expecting nil") - } -} diff --git a/pkg/helm/intern/version/clientgo.go b/pkg/helm/intern/version/clientgo.go new file mode 100644 index 00000000..ab2a38fd --- /dev/null +++ b/pkg/helm/intern/version/clientgo.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package version + +import ( + "fmt" + "runtime/debug" + "slices" + + _ "k8s.io/client-go/kubernetes" // Force k8s.io/client-go to be included in the build +) + +func K8sIOClientGoModVersion() (string, error) { + info, ok := debug.ReadBuildInfo() + if !ok { + return "", fmt.Errorf("failed to read build info") + } + + idx := slices.IndexFunc(info.Deps, func(m *debug.Module) bool { + return m.Path == "k8s.io/client-go" + }) + + if idx == -1 { + return "", fmt.Errorf("k8s.io/client-go not found in build info") + } + + m := info.Deps[idx] + + return m.Version, nil +} diff --git a/pkg/helm/intern/version/clientgo_test.go b/pkg/helm/intern/version/clientgo_test.go new file mode 100644 index 00000000..624c669a --- /dev/null +++ b/pkg/helm/intern/version/clientgo_test.go @@ -0,0 +1,30 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package version + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestK8sClientGoModVersion(t *testing.T) { + // Unfortunately, test builds don't include debug info / module info + // So we expect "K8sIOClientGoModVersion" to return error + _, err := K8sIOClientGoModVersion() + require.ErrorContains(t, err, "k8s.io/client-go not found in build info") +} diff --git a/pkg/helm/intern/version/version.go b/pkg/helm/intern/version/version.go index 414957bc..3daf8089 100644 --- a/pkg/helm/intern/version/version.go +++ b/pkg/helm/intern/version/version.go @@ -14,12 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package version // import "helm.sh/helm/v3/internal/version" +package version import ( "flag" + "fmt" + "log/slog" "runtime" "strings" + "testing" + + "github.com/Masterminds/semver/v3" ) var ( @@ -29,7 +34,7 @@ var ( // // Increment major number for new feature additions and behavioral changes. // Increment minor number for bug fixes and performance enhancements. - version = "v3.14" + version = "v4.1" // metadata is extra build time data metadata = "" @@ -39,6 +44,10 @@ var ( gitTreeState = "" ) +const ( + kubeClientGoVersionTesting = "v1.20" +) + // BuildInfo describes the compile time information. type BuildInfo struct { // Version is the current semver. @@ -49,6 +58,8 @@ type BuildInfo struct { GitTreeState string `json:"git_tree_state,omitempty"` // GoVersion is the version of the Go compiler used. GoVersion string `json:"go_version,omitempty"` + // KubeClientVersion is the version of client-go Helm was build with + KubeClientVersion string `json:"kube_client_version"` } // GetVersion returns the semver string of the version @@ -66,11 +77,39 @@ func GetUserAgent() string { // Get returns build info func Get() BuildInfo { + + makeKubeClientVersionString := func() string { + // Test builds don't include debug info / module info + // (And even if they did, we probably want a stable version during tests anyway) + // Return a default value for test builds + if testing.Testing() { + return kubeClientGoVersionTesting + } + + vstr, err := K8sIOClientGoModVersion() + if err != nil { + slog.Error("failed to retrieve k8s.io/client-go version", slog.Any("error", err)) + return "" + } + + v, err := semver.NewVersion(vstr) + if err != nil { + slog.Error("unable to parse k8s.io/client-go version", slog.String("version", vstr), slog.Any("error", err)) + return "" + } + + kubeClientVersionMajor := v.Major() + 1 + kubeClientVersionMinor := v.Minor() + + return fmt.Sprintf("v%d.%d", kubeClientVersionMajor, kubeClientVersionMinor) + } + v := BuildInfo{ - Version: GetVersion(), - GitCommit: gitCommit, - GitTreeState: gitTreeState, - GoVersion: runtime.Version(), + Version: GetVersion(), + GitCommit: gitCommit, + GitTreeState: gitTreeState, + GoVersion: runtime.Version(), + KubeClientVersion: makeKubeClientVersionString(), } // HACK(bacongobbler): strip out GoVersion during a test run for consistent test output diff --git a/pkg/helm/pkg/action/action.go b/pkg/helm/pkg/action/action.go index 91d0efae..ad835b8b 100644 --- a/pkg/helm/pkg/action/action.go +++ b/pkg/helm/pkg/action/action.go @@ -18,32 +18,42 @@ package action import ( "bytes" + "context" + "errors" "fmt" + "io" + "log/slog" + "maps" "os" "path" "path/filepath" - "regexp" + "slices" "strings" + "sync" + "text/template" + "time" - "github.com/pkg/errors" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "sigs.k8s.io/kustomize/kyaml/kio" + kyaml "sigs.k8s.io/kustomize/kyaml/yaml" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/engine" "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/postrender" + "github.com/werf/nelm/pkg/helm/pkg/postrenderer" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" + ri "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + releaseutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" "github.com/werf/nelm/pkg/helm/pkg/storage" "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - "github.com/werf/nelm/pkg/helm/pkg/time" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // Timestamper is a function capable of producing a timestamp.Timestamper. @@ -63,20 +73,20 @@ var ( errPending = errors.New("another operation (install/upgrade/rollback) is in progress") ) -// ValidName is a regular expression for resource names. -// -// DEPRECATED: This will be removed in Helm 4, and is no longer used here. See -// pkg/lint/rules.validateMetadataNameFunc for the replacement. -// -// According to the Kubernetes help text, the regular expression it uses is: -// -// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* -// -// This follows the above regular expression (but requires a full string match, not partial). -// -// The Kubernetes documentation is here, though it is not entirely correct: -// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names -var ValidName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) +type DryRunStrategy string + +const ( + // DryRunNone indicates the client will make all mutating calls + DryRunNone DryRunStrategy = "none" + + // DryRunClient, or client-side dry-run, indicates the client will avoid + // making calls to the server + DryRunClient DryRunStrategy = "client" + + // DryRunServer, or server-side dry-run, indicates the client will send + // calls to the APIServer with the dry-run parameter to prevent persisting changes + DryRunServer DryRunStrategy = "server" +) // Configuration injects the dependencies that all actions share. type Configuration struct { @@ -93,9 +103,155 @@ type Configuration struct { RegistryClient *registry.Client // Capabilities describes the capabilities of the Kubernetes cluster. - Capabilities *chartutil.Capabilities + Capabilities *common.Capabilities + + // CustomTemplateFuncs is defined by users to provide custom template funcs + CustomTemplateFuncs template.FuncMap + + // HookOutputFunc called with container name and returns and expects writer that will receive the log output. + HookOutputFunc func(namespace, pod, container string) io.Writer + + // Mutex is an exclusive lock for concurrent access to the action + mutex sync.Mutex + + // Embed a LogHolder to provide logger functionality + logging.LogHolder +} + +type ConfigurationOption func(c *Configuration) + +// Override the default logging handler +// If unspecified, the default logger will be used +func ConfigurationSetLogger(h slog.Handler) ConfigurationOption { + return func(c *Configuration) { + c.SetLogger(h) + } +} + +func NewConfiguration(options ...ConfigurationOption) *Configuration { + c := &Configuration{} + c.SetLogger(slog.Default().Handler()) + + for _, o := range options { + o(c) + } - Log func(string, ...interface{}) + return c +} + +const ( + // filenameAnnotation is the annotation key used to store the original filename + // information in manifest annotations for post-rendering reconstruction. + filenameAnnotation = "postrenderer.helm.sh/postrender-filename" +) + +// fixDocSeparators ensures YAML document separators ("---") are always +// followed by a newline in rendered template content. Go template whitespace +// trimming ({{-) can remove the newline after "---", producing e.g. +// "---apiVersion: v1" which is not a valid YAML document separator. +// This function inserts a newline after any "---" at the start of a line +// that is immediately followed by non-whitespace content. +func fixDocSeparators(content string) string { + var b strings.Builder + remaining := content + for { + // Find "---" at the start of a line (or start of content). + idx := strings.Index(remaining, "---") + if idx == -1 { + b.WriteString(remaining) + break + } + // "---" must be at the start of a line: either idx==0 or preceded by '\n'. + if idx > 0 && remaining[idx-1] != '\n' { + b.WriteString(remaining[:idx+3]) + remaining = remaining[idx+3:] + continue + } + b.WriteString(remaining[:idx+3]) + remaining = remaining[idx+3:] + // If "---" is followed by non-whitespace (e.g. "---apiVersion"), + // insert a newline to make it a proper document separator. + if len(remaining) > 0 && remaining[0] != '\n' && remaining[0] != '\r' && remaining[0] != ' ' && remaining[0] != '\t' { + b.WriteByte('\n') + } + } + return b.String() +} + +// annotateAndMerge combines multiple YAML files into a single stream of documents, +// adding filename annotations to each document for later reconstruction. +func annotateAndMerge(files map[string]string) (string, error) { + var combinedManifests []*kyaml.RNode + + // Get sorted filenames to ensure result is deterministic + fnames := slices.Sorted(maps.Keys(files)) + + for _, fname := range fnames { + content := files[fname] + // Skip partials and empty files. + if strings.HasPrefix(path.Base(fname), "_") || strings.TrimSpace(content) == "" { + continue + } + + // Fix document separators where Go template whitespace trimming + // ({{-) has removed the newline after "---", producing e.g. + // "---apiVersion: v1" which is not a valid YAML document + // separator. Insert the missing newline so kio.ParseAll can + // parse the content correctly. + content = fixDocSeparators(content) + + manifests, err := kio.ParseAll(content) + if err != nil { + return "", fmt.Errorf("parsing %s: %w", fname, err) + } + for _, manifest := range manifests { + if err := manifest.PipeE(kyaml.SetAnnotation(filenameAnnotation, fname)); err != nil { + return "", fmt.Errorf("annotating %s: %w", fname, err) + } + combinedManifests = append(combinedManifests, manifest) + } + } + + merged, err := kio.StringAll(combinedManifests) + if err != nil { + return "", fmt.Errorf("writing merged docs: %w", err) + } + return merged, nil +} + +// splitAndDeannotate reconstructs individual files from a merged YAML stream, +// removing filename annotations and grouping documents by their original filenames. +func splitAndDeannotate(postrendered string) (map[string]string, error) { + manifests, err := kio.ParseAll(postrendered) + if err != nil { + return nil, fmt.Errorf("error parsing YAML: %w", err) + } + + manifestsByFilename := make(map[string][]*kyaml.RNode) + for i, manifest := range manifests { + meta, err := manifest.GetMeta() + if err != nil { + return nil, fmt.Errorf("getting metadata: %w", err) + } + fname := meta.Annotations[filenameAnnotation] + if fname == "" { + fname = fmt.Sprintf("generated-by-postrender-%d.yaml", i) + } + if err := manifest.PipeE(kyaml.ClearAnnotation(filenameAnnotation)); err != nil { + return nil, fmt.Errorf("clearing filename annotation: %w", err) + } + manifestsByFilename[fname] = append(manifestsByFilename[fname], manifest) + } + + reconstructed := make(map[string]string, len(manifestsByFilename)) + for fname, docs := range manifestsByFilename { + fileContents, err := kio.StringAll(docs) + if err != nil { + return nil, fmt.Errorf("re-writing %s: %w", fname, err) + } + reconstructed[fname] = fileContents + } + return reconstructed, nil } // renderResources renders the templates in a chart @@ -104,8 +260,8 @@ type Configuration struct { // TODO: As part of the refactor the duplicate code in cmd/helm/template.go should be removed // // This code has to do with writing files to disk. -func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool, opts helmopts.HelmOptions) ([]*release.Hook, *bytes.Buffer, string, error) { - hs := []*release.Hook{} +func (cfg *Configuration) renderResources(ch *chart.Chart, values common.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrenderer.PostRenderer, interactWithRemote, enableDNS, hideSecret bool) ([]*release.Hook, *bytes.Buffer, string, error) { + var hs []*release.Hook b := bytes.NewBuffer(nil) caps, err := cfg.getCapabilities() @@ -115,7 +271,7 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu if ch.Metadata.KubeVersion != "" { if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { - return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) + return hs, b, "", fmt.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.Version) } } @@ -132,11 +288,15 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu } e := engine.New(restConfig) e.EnableDNS = enableDNS - files, err2 = e.Render(ch, values, opts) + e.CustomTemplateFuncs = cfg.CustomTemplateFuncs + + files, err2 = e.Render(context.Background(), ch, values) } else { var e engine.Engine e.EnableDNS = enableDNS - files, err2 = e.Render(ch, values, opts) + e.CustomTemplateFuncs = cfg.CustomTemplateFuncs + + files, err2 = e.Render(context.Background(), ch, values) } if err2 != nil { @@ -163,10 +323,37 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu } notes := notesBuffer.String() + if pr != nil { + // We need to send files to the post-renderer before sorting and splitting + // hooks from manifests. The post-renderer interface expects a stream of + // manifests (similar to what tools like Kustomize and kubectl expect), whereas + // the sorter uses filenames. + // Here, we merge the documents into a stream, post-render them, and then split + // them back into a map of filename -> content. + + // Merge files as stream of documents for sending to post renderer + merged, err := annotateAndMerge(files) + if err != nil { + return hs, b, notes, fmt.Errorf("error merging manifests: %w", err) + } + + // Run the post renderer + postRendered, err := pr.Run(bytes.NewBufferString(merged)) + if err != nil { + return hs, b, notes, fmt.Errorf("error while running post render on files: %w", err) + } + + // Use the file list and contents received from the post renderer + files, err = splitAndDeannotate(postRendered.String()) + if err != nil { + return hs, b, notes, fmt.Errorf("error while parsing post rendered output: %w", err) + } + } + // Sort hooks, manifests, and partials. Only hooks and manifests are returned, // as partials are not used after renderer.Render. Empty manifests are also // removed here. - hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) + hs, manifests, err := releaseutil.SortManifests(files, nil, releaseutil.InstallOrder) if err != nil { // By catching parse errors here, we can prevent bogus releases from going // to Kubernetes. @@ -201,7 +388,11 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu for _, m := range manifests { if outputDir == "" { - fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + if hideSecret && m.Head.Kind == "Secret" && m.Head.Version == "v1" { + fmt.Fprintf(b, "---\n# Source: %s\n# HIDDEN: The Secret output has been suppressed\n", m.Name) + } else { + fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) + } } else { newDir := outputDir if useReleaseName { @@ -219,13 +410,6 @@ func (cfg *Configuration) renderResources(ch *chart.Chart, values chartutil.Valu } } - if pr != nil { - b, err = pr.Run(b) - if err != nil { - return hs, b, notes, errors.Wrap(err, "error while running post render on files") - } - } - return hs, b, notes, nil } @@ -236,23 +420,20 @@ type RESTClientGetter interface { ToRESTMapper() (meta.RESTMapper, error) } -// DebugLog sets the logger that writes debug strings -type DebugLog func(format string, v ...interface{}) - // capabilities builds a Capabilities from discovery information. -func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { +func (cfg *Configuration) getCapabilities() (*common.Capabilities, error) { if cfg.Capabilities != nil { return cfg.Capabilities, nil } dc, err := cfg.RESTClientGetter.ToDiscoveryClient() if err != nil { - return nil, errors.Wrap(err, "could not get Kubernetes discovery client") + return nil, fmt.Errorf("could not get Kubernetes discovery client: %w", err) } // force a discovery cache invalidation to always fetch the latest server version/capabilities. dc.Invalidate() kubeVersion, err := dc.ServerVersion() if err != nil { - return nil, errors.Wrap(err, "could not get server version from Kubernetes") + return nil, fmt.Errorf("could not get server version from Kubernetes: %w", err) } // Issue #6361: // Client-Go emits an error when an API service is registered but unimplemented. @@ -262,21 +443,21 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { apiVersions, err := GetVersionSet(dc) if err != nil { if discovery.IsGroupDiscoveryFailedError(err) { - cfg.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) - cfg.Log("WARNING: To fix this, kubectl delete apiservice ") + cfg.Logger().Warn("the kubernetes server has an orphaned API service", slog.Any("error", err)) + cfg.Logger().Warn("to fix this, kubectl delete apiservice ") } else { - return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") + return nil, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) } } - cfg.Capabilities = &chartutil.Capabilities{ + cfg.Capabilities = &common.Capabilities{ APIVersions: apiVersions, - KubeVersion: chartutil.KubeVersion{ + KubeVersion: common.KubeVersion{ Version: kubeVersion.GitVersion, Major: kubeVersion.Major, Minor: kubeVersion.Minor, }, - HelmVersion: chartutil.DefaultCapabilities.HelmVersion, + HelmVersion: common.DefaultCapabilities.HelmVersion, } return cfg.Capabilities, nil } @@ -285,7 +466,7 @@ func (cfg *Configuration) getCapabilities() (*chartutil.Capabilities, error) { func (cfg *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { conf, err := cfg.RESTClientGetter.ToRESTConfig() if err != nil { - return nil, errors.Wrap(err, "unable to generate config for kubernetes client") + return nil, fmt.Errorf("unable to generate config for kubernetes client: %w", err) } return kubernetes.NewForConfig(conf) @@ -299,9 +480,9 @@ func (cfg *Configuration) Now() time.Time { return Timestamper() } -func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) { +func (cfg *Configuration) releaseContent(name string, version int) (ri.Releaser, error) { if err := chartutil.ValidateReleaseName(name); err != nil { - return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) + return nil, fmt.Errorf("releaseContent: Release name is invalid: %s", name) } if version <= 0 { @@ -312,10 +493,10 @@ func (cfg *Configuration) releaseContent(name string, version int) (*release.Rel } // GetVersionSet retrieves a set of available k8s API versions -func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.VersionSet, error) { +func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet, error) { groups, resources, err := client.ServerGroupsAndResources() if err != nil && !discovery.IsGroupDiscoveryFailedError(err) { - return chartutil.DefaultVersionSet, errors.Wrap(err, "could not get apiVersions from Kubernetes") + return common.DefaultVersionSet, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) } // FIXME: The Kubernetes test fixture for cli appears to always return nil @@ -323,11 +504,11 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version // return the default API list. This is also a safe value to return in any // other odd-ball case. if len(groups) == 0 && len(resources) == 0 { - return chartutil.DefaultVersionSet, nil + return common.DefaultVersionSet, nil } versionMap := make(map[string]interface{}) - versions := []string{} + var versions []string // Extract the groups for _, g := range groups { @@ -356,20 +537,25 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (chartutil.Version versions = append(versions, k) } - return chartutil.VersionSet(versions), nil + return common.VersionSet(versions), nil } // recordRelease with an update operation in case reuse has been set. func (cfg *Configuration) recordRelease(r *release.Release) { if err := cfg.Releases.Update(r); err != nil { - cfg.Log("warning: Failed to update release %s: %s", r.Name, err) + cfg.Logger().Warn( + "failed to update release", + slog.String("name", r.Name), + slog.Int("revision", r.Version), + slog.Any("error", err), + ) } } // Init initializes the action configuration -func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string, log DebugLog) error { +func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string) error { kc := kube.New(getter) - kc.Log = log + kc.SetLogger(cfg.Logger().Handler()) lazyClient := &lazyClient{ namespace: namespace, @@ -380,58 +566,68 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp switch helmDriver { case "secret", "secrets", "": d := driver.NewSecrets(newSecretClient(lazyClient)) - d.Log = log + d.SetLogger(cfg.Logger().Handler()) store = storage.Init(d) case "configmap", "configmaps": d := driver.NewConfigMaps(newConfigMapClient(lazyClient)) - d.Log = log + d.SetLogger(cfg.Logger().Handler()) store = storage.Init(d) case "memory": var d *driver.Memory if cfg.Releases != nil { if mem, ok := cfg.Releases.Driver.(*driver.Memory); ok { // This function can be called more than once (e.g., helm list --all-namespaces). - // If a memory driver was already initialized, re-use it but set the possibly new namespace. - // We re-use it in case some releases where already created in the existing memory driver. + // If a memory driver was already initialized, reuse it but set the possibly new namespace. + // We reuse it in case some releases where already created in the existing memory driver. d = mem } } if d == nil { d = driver.NewMemory() } + d.SetLogger(cfg.Logger().Handler()) d.SetNamespace(namespace) store = storage.Init(d) case "sql": d, err := driver.NewSQL( os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"), - log, namespace, ) if err != nil { - panic(fmt.Sprintf("Unable to instantiate SQL driver: %v", err)) + return fmt.Errorf("unable to instantiate SQL driver: %w", err) } + d.SetLogger(cfg.Logger().Handler()) store = storage.Init(d) default: - // Not sure what to do here. - panic("Unknown driver in HELM_DRIVER: " + helmDriver) + return fmt.Errorf("unknown driver %q", helmDriver) } cfg.RESTClientGetter = getter cfg.KubeClient = kc cfg.Releases = store - cfg.Log = log + cfg.HookOutputFunc = func(_, _, _ string) io.Writer { return io.Discard } return nil } -func (cfg *Configuration) RenderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, interactWithRemote, enableDNS bool, opts helmopts.HelmOptions) ([]*release.Hook, *bytes.Buffer, string, error) { - return cfg.renderResources(ch, values, releaseName, outputDir, subNotes, useReleaseName, includeCrds, pr, interactWithRemote, enableDNS, opts) +// SetHookOutputFunc sets the HookOutputFunc on the Configuration. +func (cfg *Configuration) SetHookOutputFunc(hookOutputFunc func(_, _, _ string) io.Writer) { + cfg.HookOutputFunc = hookOutputFunc +} + +func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod { + if serverSideApply { + return release.ApplyMethodServerSideApply + } + return release.ApplyMethodClientSideApply } -func (cfg *Configuration) GetCapabilities() (*chartutil.Capabilities, error) { - return cfg.getCapabilities() +// isDryRun returns true if the strategy is set to run as a DryRun +func isDryRun(strategy DryRunStrategy) bool { + return strategy == DryRunClient || strategy == DryRunServer } -func ErrMissingChart() error { - return errMissingChart +// interactWithServer determine whether or not to interact with a remote Kubernetes server +func interactWithServer(strategy DryRunStrategy) bool { + return strategy == DryRunNone || strategy == DryRunServer } diff --git a/pkg/helm/pkg/action/action_test.go b/pkg/helm/pkg/action/action_test.go index f6b288f1..499fac10 100644 --- a/pkg/helm/pkg/action/action_test.go +++ b/pkg/helm/pkg/action/action_test.go @@ -16,26 +16,46 @@ limitations under the License. package action import ( + "bytes" + "errors" "flag" + "fmt" "io" + "log/slog" + "strings" "testing" + "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" fakeclientset "k8s.io/client-go/kubernetes/fake" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/kube" kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/release" + rcommon "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/helm/pkg/storage" "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - "github.com/werf/nelm/pkg/helm/pkg/time" ) -var verbose = flag.Bool("test.log", false, "enable test logging") +var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)") func actionConfigFixture(t *testing.T) *Configuration { t.Helper() + return actionConfigFixtureWithDummyResources(t, nil) +} + +func actionConfigFixtureWithDummyResources(t *testing.T, dummyResources kube.ResourceList) *Configuration { + t.Helper() + + logger := logging.NewLogger(func() bool { + return *verbose + }) + slog.SetDefault(logger) registryClient, err := registry.NewClient() if err != nil { @@ -44,15 +64,9 @@ func actionConfigFixture(t *testing.T) *Configuration { return &Configuration{ Releases: storage.Init(driver.NewMemory()), - KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}}, - Capabilities: chartutil.DefaultCapabilities, + KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: dummyResources}, + Capabilities: common.DefaultCapabilities, RegistryClient: registryClient, - Log: func(format string, v ...interface{}) { - t.Helper() - if *verbose { - t.Logf(format, v...) - } - }, } } @@ -109,6 +123,15 @@ type chartOptions struct { type chartOption func(*chartOptions) func buildChart(opts ...chartOption) *chart.Chart { + modTime := time.Now() + defaultTemplates := []*common.File{ + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifestWithHook)}, + } + return buildChartWithTemplates(defaultTemplates, opts...) +} + +func buildChartWithTemplates(templates []*common.File, opts ...chartOption) *chart.Chart { c := &chartOptions{ Chart: &chart.Chart{ // TODO: This should be more complete. @@ -117,18 +140,13 @@ func buildChart(opts ...chartOption) *chart.Chart { Name: "hello", Version: "0.1.0", }, - // This adds a basic template and hooks. - Templates: []*chart.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, - }, + Templates: templates, }, } for _, opt := range opts { opt(c) } - return c.Chart } @@ -163,9 +181,10 @@ func withValues(values map[string]interface{}) chartOption { func withNotes(notes string) chartOption { return func(opts *chartOptions) { - opts.Templates = append(opts.Templates, &chart.File{ - Name: "templates/NOTES.txt", - Data: []byte(notes), + opts.Templates = append(opts.Templates, &common.File{ + Name: "templates/NOTES.txt", + ModTime: time.Now(), + Data: []byte(notes), }) } } @@ -182,28 +201,43 @@ func withMetadataDependency(dependency chart.Dependency) chartOption { } } +func withFile(file common.File) chartOption { + return func(opts *chartOptions) { + opts.Files = append(opts.Files, &file) + } +} + func withSampleTemplates() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + modTime := time.Now() + sampleTemplates := []*common.File{ // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")}, + {Name: "templates/empty", ModTime: modTime, Data: []byte("")}, + {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } } +func withSampleSecret() chartOption { + return func(opts *chartOptions) { + sampleSecret := &common.File{Name: "templates/secret.yaml", ModTime: time.Now(), Data: []byte("apiVersion: v1\nkind: Secret\n")} + opts.Templates = append(opts.Templates, sampleSecret) + } +} + func withSampleIncludingIncorrectTemplates() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ + modTime := time.Now() + sampleTemplates := []*common.File{ // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")}, + {Name: "templates/empty", ModTime: modTime, Data: []byte("")}, + {Name: "templates/incorrect", ModTime: modTime, Data: []byte("{{ .Values.bad.doh }}")}, + {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -211,8 +245,8 @@ func withSampleIncludingIncorrectTemplates() chartOption { func withMultipleManifestTemplate() chartOption { return func(opts *chartOptions) { - sampleTemplates := []*chart.File{ - {Name: "templates/rbac", Data: []byte(rbacManifests)}, + sampleTemplates := []*common.File{ + {Name: "templates/rbac", ModTime: time.Now(), Data: []byte(rbacManifests)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -226,10 +260,10 @@ func withKube(version string) chartOption { // releaseStub creates a release stub, complete with the chartStub as its chart. func releaseStub() *release.Release { - return namedReleaseStub("angry-panda", release.StatusDeployed) + return namedReleaseStub("angry-panda", rcommon.StatusDeployed) } -func namedReleaseStub(name string, status release.Status) *release.Release { +func namedReleaseStub(name string, status rcommon.Status) *release.Release { now := time.Now() return &release.Release{ Name: name, @@ -266,8 +300,76 @@ func namedReleaseStub(name string, status release.Status) *release.Release { } } +func TestConfiguration_Init(t *testing.T) { + tests := []struct { + name string + helmDriver string + expectedDriverType interface{} + expectErr bool + errMsg string + }{ + { + name: "Test secret driver", + helmDriver: "secret", + expectedDriverType: &driver.Secrets{}, + }, + { + name: "Test secrets driver", + helmDriver: "secrets", + expectedDriverType: &driver.Secrets{}, + }, + { + name: "Test empty driver", + helmDriver: "", + expectedDriverType: &driver.Secrets{}, + }, + { + name: "Test configmap driver", + helmDriver: "configmap", + expectedDriverType: &driver.ConfigMaps{}, + }, + { + name: "Test configmaps driver", + helmDriver: "configmaps", + expectedDriverType: &driver.ConfigMaps{}, + }, + { + name: "Test memory driver", + helmDriver: "memory", + expectedDriverType: &driver.Memory{}, + }, + { + name: "Test sql driver", + helmDriver: "sql", + expectErr: true, + errMsg: "unable to instantiate SQL driver", + }, + { + name: "Test unknown driver", + helmDriver: "someDriver", + expectErr: true, + errMsg: fmt.Sprintf("unknown driver %q", "someDriver"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := NewConfiguration() + + actualErr := cfg.Init(nil, "default", tt.helmDriver) + if tt.expectErr { + assert.Error(t, actualErr) + assert.Contains(t, actualErr.Error(), tt.errMsg) + } else { + assert.NoError(t, actualErr) + assert.IsType(t, tt.expectedDriverType, cfg.Releases.Driver) + } + }) + } +} + func TestGetVersionSet(t *testing.T) { - client := fakeclientset.NewSimpleClientset() + client := fakeclientset.NewClientset() vs, err := GetVersionSet(client.Discovery()) if err != nil { @@ -281,3 +383,743 @@ func TestGetVersionSet(t *testing.T) { t.Error("Non-existent version is reported found.") } } + +// Mock PostRenderer for testing +type mockPostRenderer struct { + shouldError bool + transform func(string) string +} + +func (m *mockPostRenderer) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { + if m.shouldError { + return nil, errors.New("mock post-renderer error") + } + + content := renderedManifests.String() + if m.transform != nil { + content = m.transform(content) + } + + return bytes.NewBufferString(content), nil +} + +func TestFixDocSeparators(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "no separator", + input: "apiVersion: v1\nkind: Service\n", + expected: "apiVersion: v1\nkind: Service\n", + }, + { + name: "separator on its own line", + input: "---\napiVersion: v1\nkind: Service\n", + expected: "---\napiVersion: v1\nkind: Service\n", + }, + { + name: "leading separator glued to content", + input: "---apiVersion: v1\nkind: Service\n", + expected: "---\napiVersion: v1\nkind: Service\n", + }, + { + name: "mid-content separator glued to content", + input: "apiVersion: v1\nkind: ConfigMap\n---apiVersion: v1\nkind: Service\n", + expected: "apiVersion: v1\nkind: ConfigMap\n---\napiVersion: v1\nkind: Service\n", + }, + { + name: "multiple separators all proper", + input: "---\napiVersion: v1\n---\napiVersion: v1\n", + expected: "---\napiVersion: v1\n---\napiVersion: v1\n", + }, + { + name: "multiple separators some glued", + input: "---apiVersion: v1\nkind: ConfigMap\n---apiVersion: v1\nkind: Service\n", + expected: "---\napiVersion: v1\nkind: ConfigMap\n---\napiVersion: v1\nkind: Service\n", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "only separator", + input: "---\n", + expected: "---\n", + }, + { + name: "triple dash in a value is not a separator", + input: "data:\n key: ---value\n", + expected: "data:\n key: ---value\n", + }, + { + name: "realistic multi-doc template output", + input: "apiVersion: v1\nkind: Deployment\n---\napiVersion: v1\nkind: Ingress\n---apiVersion: v1\nkind: Service\n", + expected: "apiVersion: v1\nkind: Deployment\n---\napiVersion: v1\nkind: Ingress\n---\napiVersion: v1\nkind: Service\n", + }, + { + name: "separator followed by carriage return", + input: "---\r\napiVersion: v1\n", + expected: "---\r\napiVersion: v1\n", + }, + { + name: "separator followed by space", + input: "--- \napiVersion: v1\n", + expected: "--- \napiVersion: v1\n", + }, + { + name: "separator followed by tab", + input: "---\t\napiVersion: v1\n", + expected: "---\t\napiVersion: v1\n", + }, + { + name: "four dashes on its own line", + input: "----\napiVersion: v1\n", + expected: "---\n-\napiVersion: v1\n", + }, + { + name: "four dashes followed by text", + input: "----more\napiVersion: v1\n", + expected: "---\n-more\napiVersion: v1\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, fixDocSeparators(tt.input)) + }) + } +} + +func TestAnnotateAndMerge(t *testing.T) { + tests := []struct { + name string + files map[string]string + expectedError string + expected string + }{ + { + name: "no files", + files: map[string]string{}, + expected: "", + }, + { + name: "single file with single manifest", + files: map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/configmap.yaml' +data: + key: value +`, + }, + { + name: "multiple files with multiple manifests", + files: map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value`, + "templates/secret.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: test-secret +data: + password: dGVzdA==`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/configmap.yaml' +data: + key: value +--- +apiVersion: v1 +kind: Secret +metadata: + name: test-secret + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/secret.yaml' +data: + password: dGVzdA== +`, + }, + { + name: "file with multiple manifests", + files: map[string]string{ + "templates/multi.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 +data: + key: value1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm2 +data: + key: value2`, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/multi.yaml' +data: + key: value1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm2 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/multi.yaml' +data: + key: value2 +`, + }, + { + name: "partials and empty files are removed", + files: map[string]string{ + "templates/cm.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 +`, + "templates/_partial.tpl": ` +{{-define name}} + {{- "abracadabra"}} +{{- end -}}`, + "templates/empty.yaml": ``, + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/cm.yaml' +`, + }, + { + name: "empty file", + files: map[string]string{ + "templates/empty.yaml": "", + }, + expected: ``, + }, + { + name: "invalid yaml", + files: map[string]string{ + "templates/invalid.yaml": `invalid: yaml: content: + - malformed`, + }, + expectedError: "parsing templates/invalid.yaml", + }, + { + name: "leading doc separator glued to content by template whitespace trimming", + files: map[string]string{ + "templates/service.yaml": "---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + }, + expected: `apiVersion: v1 +kind: Service +metadata: + name: test-svc + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/service.yaml' +`, + }, + { + name: "leading doc separator on its own line", + files: map[string]string{ + "templates/service.yaml": "---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + }, + expected: `apiVersion: v1 +kind: Service +metadata: + name: test-svc + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/service.yaml' +`, + }, + { + name: "multiple leading doc separators", + files: map[string]string{ + "templates/service.yaml": "---\n---\napiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + }, + expected: `apiVersion: v1 +kind: Service +metadata: + name: test-svc + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/service.yaml' +`, + }, + { + name: "mid-content doc separator glued to content by template whitespace trimming", + files: map[string]string{ + "templates/all.yaml": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: test-cm\n---apiVersion: v1\nkind: Service\nmetadata:\n name: test-svc\n", + }, + expected: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/all.yaml' +--- +apiVersion: v1 +kind: Service +metadata: + name: test-svc + annotations: + postrenderer.helm.sh/postrender-filename: 'templates/all.yaml' +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + merged, err := annotateAndMerge(tt.files) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.NotNil(t, merged) + assert.Equal(t, tt.expected, merged) + } + }) + } +} + +func TestSplitAndDeannotate(t *testing.T) { + tests := []struct { + name string + input string + expectedFiles map[string]string + expectedError string + }{ + { + name: "single annotated manifest", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + postrenderer.helm.sh/postrender-filename: templates/configmap.yaml +data: + key: value`, + expectedFiles: map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value +`, + }, + }, + { + name: "multiple manifests with different filenames", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + postrenderer.helm.sh/postrender-filename: templates/configmap.yaml +data: + key: value +--- +apiVersion: v1 +kind: Secret +metadata: + name: test-secret + annotations: + postrenderer.helm.sh/postrender-filename: templates/secret.yaml +data: + password: dGVzdA==`, + expectedFiles: map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value +`, + "templates/secret.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: test-secret +data: + password: dGVzdA== +`, + }, + }, + { + name: "multiple manifests with same filename", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 + annotations: + postrenderer.helm.sh/postrender-filename: templates/multi.yaml +data: + key: value1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm2 + annotations: + postrenderer.helm.sh/postrender-filename: templates/multi.yaml +data: + key: value2`, + expectedFiles: map[string]string{ + "templates/multi.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 +data: + key: value1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm2 +data: + key: value2 +`, + }, + }, + { + name: "manifest with other annotations", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + postrenderer.helm.sh/postrender-filename: templates/configmap.yaml + other-annotation: should-remain +data: + key: value`, + expectedFiles: map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm + annotations: + other-annotation: should-remain +data: + key: value +`, + }, + }, + { + name: "invalid yaml input", + input: "invalid: yaml: content:", + expectedError: "error parsing YAML: MalformedYAMLError", + }, + { + name: "manifest without filename annotation", + input: `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value`, + expectedFiles: map[string]string{ + "generated-by-postrender-0.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value +`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + files, err := splitAndDeannotate(tt.input) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + } else { + assert.NoError(t, err) + assert.Equal(t, len(tt.expectedFiles), len(files)) + + for expectedFile, expectedContent := range tt.expectedFiles { + actualContent, exists := files[expectedFile] + assert.True(t, exists, "Expected file %s not found", expectedFile) + assert.Equal(t, expectedContent, actualContent) + } + } + }) + } +} + +func TestAnnotateAndMerge_SplitAndDeannotate_Roundtrip(t *testing.T) { + // Test that merge/split operations are symmetric + originalFiles := map[string]string{ + "templates/configmap.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm +data: + key: value`, + "templates/secret.yaml": `apiVersion: v1 +kind: Secret +metadata: + name: test-secret +data: + password: dGVzdA==`, + "templates/multi.yaml": `apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm1 +data: + key: value1 +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-cm2 +data: + key: value2`, + } + + // Merge and annotate + merged, err := annotateAndMerge(originalFiles) + require.NoError(t, err) + + // Split and deannotate + reconstructed, err := splitAndDeannotate(merged) + require.NoError(t, err) + + // Compare the results + assert.Equal(t, len(originalFiles), len(reconstructed)) + for filename, originalContent := range originalFiles { + reconstructedContent, exists := reconstructed[filename] + assert.True(t, exists, "File %s should exist in reconstructed files", filename) + + // Normalize whitespace for comparison since YAML processing might affect formatting + normalizeContent := func(content string) string { + return strings.TrimSpace(strings.ReplaceAll(content, "\r\n", "\n")) + } + + assert.Equal(t, normalizeContent(originalContent), normalizeContent(reconstructedContent)) + } +} + +func TestRenderResources_PostRenderer_Success(t *testing.T) { + cfg := actionConfigFixture(t) + + // Create a simple mock post-renderer + mockPR := &mockPostRenderer{ + transform: func(content string) string { + content = strings.ReplaceAll(content, "hello", "yellow") + content = strings.ReplaceAll(content, "goodbye", "foodpie") + return strings.ReplaceAll(content, "test-cm", "test-cm-postrendered") + }, + } + + ch := buildChart(withSampleTemplates()) + values := map[string]interface{}{} + + hooks, buf, notes, err := cfg.renderResources( + ch, values, "test-release", "", false, false, false, + mockPR, false, false, false, + ) + + assert.NoError(t, err) + assert.NotNil(t, hooks) + assert.NotNil(t, buf) + assert.Equal(t, "", notes) + expectedBuf := `--- +# Source: yellow/templates/foodpie +foodpie: world +--- +# Source: yellow/templates/with-partials +yellow: Earth +--- +# Source: yellow/templates/yellow +yellow: world +` + expectedHook := `kind: ConfigMap +metadata: + name: test-cm-postrendered + annotations: + "helm.sh/hook": post-install,pre-delete,post-upgrade +data: + name: value` + + assert.Equal(t, expectedBuf, buf.String()) + assert.Len(t, hooks, 1) + assert.Equal(t, expectedHook, hooks[0].Manifest) +} + +func TestRenderResources_PostRenderer_Error(t *testing.T) { + cfg := actionConfigFixture(t) + + // Create a post-renderer that returns an error + mockPR := &mockPostRenderer{ + shouldError: true, + } + + ch := buildChart(withSampleTemplates()) + values := map[string]interface{}{} + + _, _, _, err := cfg.renderResources( + ch, values, "test-release", "", false, false, false, + mockPR, false, false, false, + ) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "error while running post render on files") +} + +func TestRenderResources_PostRenderer_MergeError(t *testing.T) { + cfg := actionConfigFixture(t) + + // Create a mock post-renderer + mockPR := &mockPostRenderer{} + + // Create a chart with invalid YAML that would cause AnnotateAndMerge to fail + ch := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v1", + Name: "test-chart", + Version: "0.1.0", + }, + Templates: []*common.File{ + {Name: "templates/invalid", ModTime: time.Now(), Data: []byte("invalid: yaml: content:")}, + }, + } + values := map[string]interface{}{} + + _, _, _, err := cfg.renderResources( + ch, values, "test-release", "", false, false, false, + mockPR, false, false, false, + ) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "error merging manifests") +} + +func TestRenderResources_PostRenderer_SplitError(t *testing.T) { + cfg := actionConfigFixture(t) + + // Create a post-renderer that returns invalid YAML + mockPR := &mockPostRenderer{ + transform: func(_ string) string { + return "invalid: yaml: content:" + }, + } + + ch := buildChart(withSampleTemplates()) + values := map[string]interface{}{} + + _, _, _, err := cfg.renderResources( + ch, values, "test-release", "", false, false, false, + mockPR, false, false, false, + ) + + assert.Error(t, err) + assert.Contains(t, err.Error(), "error while parsing post rendered output: error parsing YAML: MalformedYAMLError:") +} + +func TestRenderResources_PostRenderer_Integration(t *testing.T) { + cfg := actionConfigFixture(t) + + mockPR := &mockPostRenderer{ + transform: func(content string) string { + return strings.ReplaceAll(content, "metadata:", "color: blue\nmetadata:") + }, + } + + ch := buildChart(withSampleTemplates()) + values := map[string]interface{}{} + + hooks, buf, notes, err := cfg.renderResources( + ch, values, "test-release", "", false, false, false, + mockPR, false, false, false, + ) + + assert.NoError(t, err) + assert.NotNil(t, hooks) + assert.NotNil(t, buf) + assert.Equal(t, "", notes) // Notes should be empty for this test + + // Verify that the post-renderer modifications are present in the output + output := buf.String() + expected := `--- +# Source: hello/templates/goodbye +goodbye: world +color: blue +--- +# Source: hello/templates/hello +hello: world +color: blue +--- +# Source: hello/templates/with-partials +hello: Earth +color: blue +` + assert.Contains(t, output, "color: blue") + assert.Equal(t, 3, strings.Count(output, "color: blue")) + assert.Equal(t, expected, output) +} + +func TestRenderResources_NoPostRenderer(t *testing.T) { + cfg := actionConfigFixture(t) + + ch := buildChart(withSampleTemplates()) + values := map[string]interface{}{} + + hooks, buf, notes, err := cfg.renderResources( + ch, values, "test-release", "", false, false, false, + nil, false, false, false, + ) + + assert.NoError(t, err) + assert.NotNil(t, hooks) + assert.NotNil(t, buf) + assert.Equal(t, "", notes) +} + +func TestDetermineReleaseSSAApplyMethod(t *testing.T) { + assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false)) + assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true)) +} + +func TestIsDryRun(t *testing.T) { + assert.False(t, isDryRun(DryRunNone)) + assert.True(t, isDryRun(DryRunClient)) + assert.True(t, isDryRun(DryRunServer)) +} + +func TestInteractWithServer(t *testing.T) { + assert.True(t, interactWithServer(DryRunNone)) + assert.False(t, interactWithServer(DryRunClient)) + assert.True(t, interactWithServer(DryRunServer)) +} diff --git a/pkg/helm/pkg/action/dependency.go b/pkg/helm/pkg/action/dependency.go index 66d244b5..a2c786f8 100644 --- a/pkg/helm/pkg/action/dependency.go +++ b/pkg/helm/pkg/action/dependency.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "context" "fmt" "io" "os" @@ -26,19 +27,25 @@ import ( "github.com/Masterminds/semver/v3" "github.com/gosuri/uitable" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" ) // Dependency is the action for building a given chart's dependency tree. // // It provides the implementation of 'helm dependency' and its respective subcommands. type Dependency struct { - Verify bool - Keyring string - SkipRefresh bool - ColumnWidth uint + Verify bool + Keyring string + SkipRefresh bool + ColumnWidth uint + Username string + Password string + CertFile string + KeyFile string + CaFile string + InsecureSkipTLSVerify bool + PlainHTTP bool } // NewDependency creates a new Dependency object with the given configuration. @@ -49,8 +56,8 @@ func NewDependency() *Dependency { } // List executes 'helm dependency list'. -func (d *Dependency) List(chartpath string, out io.Writer, opts helmopts.HelmOptions) error { - c, err := loader.Load(chartpath, opts) +func (d *Dependency) List(chartpath string, out io.Writer) error { + c, err := loader.Load(context.Background(), chartpath) if err != nil { return err } @@ -60,14 +67,14 @@ func (d *Dependency) List(chartpath string, out io.Writer, opts helmopts.HelmOpt return nil } - d.printDependencies(chartpath, out, c, opts) + d.printDependencies(chartpath, out, c) fmt.Fprintln(out) - d.printMissing(chartpath, out, c.Metadata.Dependencies, opts) + d.printMissing(chartpath, out, c.Metadata.Dependencies) return nil } // dependencyStatus returns a string describing the status of a dependency viz a viz the parent chart. -func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart, opts helmopts.HelmOptions) string { +func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string { filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") // If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs. @@ -97,7 +104,7 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p if l := len(found); l == 1 { // If we get here, we do the same thing as in len(archives) == 1. - if r := statArchiveForStatus(found[0], dep, opts); r != "" { + if r := statArchiveForStatus(found[0], dep); r != "" { return r } @@ -111,7 +118,7 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p case len(archives) == 1: archive := archives[0] - if r := statArchiveForStatus(archive, dep, opts); r != "" { + if r := statArchiveForStatus(archive, dep); r != "" { return r } @@ -152,9 +159,9 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p // // This is a refactor of the code originally in dependencyStatus. It is here to // support legacy behavior, and should be removed in Helm 4. -func statArchiveForStatus(archive string, dep *chart.Dependency, opts helmopts.HelmOptions) string { +func statArchiveForStatus(archive string, dep *chart.Dependency) string { if _, err := os.Stat(archive); err == nil { - c, err := loader.Load(archive, opts) + c, err := loader.Load(context.Background(), archive) if err != nil { return "corrupt" } @@ -183,19 +190,19 @@ func statArchiveForStatus(archive string, dep *chart.Dependency, opts helmopts.H } // printDependencies prints all of the dependencies in the yaml file. -func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart, opts helmopts.HelmOptions) { +func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) { table := uitable.New() table.MaxColWidth = d.ColumnWidth table.AddRow("NAME", "VERSION", "REPOSITORY", "STATUS") for _, row := range c.Metadata.Dependencies { - table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c, opts)) + table.AddRow(row.Name, row.Version, row.Repository, d.dependencyStatus(chartpath, row, c)) } fmt.Fprintln(out, table) } // printMissing prints warnings about charts that are present on disk, but are // not in Chart.yaml. -func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency, opts helmopts.HelmOptions) { +func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart.Dependency) { folder := filepath.Join(chartpath, "charts/*") files, err := filepath.Glob(folder) if err != nil { @@ -212,7 +219,7 @@ func (d *Dependency) printMissing(chartpath string, out io.Writer, reqs []*chart if !fi.IsDir() && filepath.Ext(f) != ".tgz" { continue } - c, err := loader.Load(f, opts) + c, err := loader.Load(context.Background(), f) if err != nil { fmt.Fprintf(out, "WARNING: %q is not a chart.\n", f) continue diff --git a/pkg/helm/pkg/action/dependency_test.go b/pkg/helm/pkg/action/dependency_test.go index a48b5f02..d5691610 100644 --- a/pkg/helm/pkg/action/dependency_test.go +++ b/pkg/helm/pkg/action/dependency_test.go @@ -25,8 +25,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/werf/nelm/pkg/helm/intern/test" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" ) func TestList(t *testing.T) { diff --git a/pkg/helm/pkg/action/exports.go b/pkg/helm/pkg/action/exports.go deleted file mode 100644 index 075968cc..00000000 --- a/pkg/helm/pkg/action/exports.go +++ /dev/null @@ -1,17 +0,0 @@ -package action - -import "k8s.io/client-go/kubernetes" - -const NotesFileSuffix = notesFileSuffix - -var ( - NewSecretClient = newSecretClient - NewConfigMapClient = newConfigMapClient -) - -func NewLazyClient(namespace string, clientFn func() (*kubernetes.Clientset, error)) *lazyClient { - return &lazyClient{ - namespace: namespace, - clientFn: clientFn, - } -} diff --git a/pkg/helm/pkg/action/get_values.go b/pkg/helm/pkg/action/get_values.go new file mode 100644 index 00000000..dae118d1 --- /dev/null +++ b/pkg/helm/pkg/action/get_values.go @@ -0,0 +1,37 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + + release "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { + switch r := rel.(type) { + case rspb.Release: + return &r, nil + case *rspb.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} diff --git a/pkg/helm/pkg/action/history.go b/pkg/helm/pkg/action/history.go index c7f38b56..fb4b911d 100644 --- a/pkg/helm/pkg/action/history.go +++ b/pkg/helm/pkg/action/history.go @@ -17,9 +17,9 @@ limitations under the License. package action import ( - "github.com/pkg/errors" + "fmt" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/release" ) @@ -44,15 +44,15 @@ func NewHistory(cfg *Configuration) *History { } // Run executes 'helm history' against the given release. -func (h *History) Run(name string) ([]*release.Release, error) { +func (h *History) Run(name string) ([]release.Releaser, error) { if err := h.cfg.KubeClient.IsReachable(); err != nil { return nil, err } if err := chartutil.ValidateReleaseName(name); err != nil { - return nil, errors.Errorf("release name is invalid: %s", name) + return nil, fmt.Errorf("release name is invalid: %s", name) } - h.cfg.Log("getting history for release %s", name) + h.cfg.Logger().Debug("getting history for release", "release", name) return h.cfg.Releases.History(name) } diff --git a/pkg/helm/pkg/action/history_test.go b/pkg/helm/pkg/action/history_test.go new file mode 100644 index 00000000..b45ed8e8 --- /dev/null +++ b/pkg/helm/pkg/action/history_test.go @@ -0,0 +1,108 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" +) + +func TestNewHistory(t *testing.T) { + config := actionConfigFixture(t) + client := NewHistory(config) + + assert.NotNil(t, client) + assert.Equal(t, config, client.cfg) +} + +func TestHistoryRun(t *testing.T) { + releaseName := "test-release" + simpleRelease := namedReleaseStub(releaseName, common.StatusPendingUpgrade) + updatedRelease := namedReleaseStub(releaseName, common.StatusDeployed) + updatedRelease.Chart.Metadata.Version = "0.1.1" + updatedRelease.Version = 2 + + config := actionConfigFixture(t) + client := NewHistory(config) + client.Max = 3 + client.cfg.Releases.MaxHistory = 3 + for _, rel := range []*release.Release{simpleRelease, updatedRelease} { + if err := client.cfg.Releases.Create(rel); err != nil { + t.Fatal(err, "Could not add releases to Config") + } + } + + releases, err := config.Releases.ListReleases() + require.NoError(t, err) + assert.Len(t, releases, 2, "expected 2 Releases in Config") + + releasers, err := client.Run(releaseName) + require.NoError(t, err) + assert.Len(t, releasers, 2, "expected 2 Releases in History result") + + release1, err := releaserToV1Release(releasers[0]) + require.NoError(t, err) + assert.Equal(t, simpleRelease.Name, release1.Name) + assert.Equal(t, simpleRelease.Version, release1.Version) + + release2, err := releaserToV1Release(releasers[1]) + require.NoError(t, err) + assert.Equal(t, updatedRelease.Name, release2.Name) + assert.Equal(t, updatedRelease.Version, release2.Version) +} + +func TestHistoryRun_UnreachableKubeClient(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + client := NewHistory(config) + result, err := client.Run("release-name") + assert.Nil(t, result) + assert.Error(t, err) +} + +func TestHistoryRun_InvalidReleaseNames(t *testing.T) { + config := actionConfigFixture(t) + client := NewHistory(config) + invalidReleaseNames := []string{ + "", + "too-long-release-name-max-53-characters-abcdefghijklmnopqrstuvwxyz", + "MyRelease", + "release_name", + "release@123", + "-badstart", + "badend-", + ".dotstart", + } + + for _, name := range invalidReleaseNames { + result, err := client.Run(name) + assert.Nil(t, result) + assert.ErrorContains(t, err, "release name is invalid") + } +} diff --git a/pkg/helm/pkg/action/hooks.go b/pkg/helm/pkg/action/hooks.go deleted file mode 100644 index 5aaf3492..00000000 --- a/pkg/helm/pkg/action/hooks.go +++ /dev/null @@ -1,181 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "bytes" - "fmt" - "sort" - "strings" - "time" - - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/release" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" -) - -// execHook executes all of the hooks for the given hook event. -func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, timeout time.Duration) error { - executingHooks := []*release.Hook{} - - for _, h := range rl.Hooks { - for _, e := range h.Events { - if e == hook { - executingHooks = append(executingHooks, h) - } - } - } - - // hooke are pre-ordered by kind, so keep order stable - sort.Stable(hookByWeight(executingHooks)) - - for i, h := range executingHooks { - // Set default delete policy to before-hook-creation - if h.DeletePolicies == nil || len(h.DeletePolicies) == 0 { - // TODO(jlegrone): Only apply before-hook-creation delete policy to run to completion - // resources. For all other resource types update in place if a - // resource with the same name already exists and is owned by the - // current release. - h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation} - } - - if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, timeout); err != nil { - return err - } - - resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), true) - if err != nil { - return errors.Wrapf(err, "unable to build kubernetes object for %s hook %s", hook, h.Path) - } - - // Record the time at which the hook was applied to the cluster - h.LastRun = release.HookExecution{ - StartedAt: helmtime.Now(), - Phase: release.HookPhaseRunning, - } - - if err := cfg.Releases.Update(release.SetHookPhaseStageInfo(rl, i, hook)); err != nil { - return fmt.Errorf("error recording release: %w", err) - } - - // As long as the implementation of WatchUntilReady does not panic, HookPhaseFailed or HookPhaseSucceeded - // should always be set by this function. If we fail to do that for any reason, then HookPhaseUnknown is - // the most appropriate value to surface. - h.LastRun.Phase = release.HookPhaseUnknown - - // Create hook resources - if _, err := cfg.KubeClient.Create(resources, kube.CreateOptions{}); err != nil { - h.LastRun.CompletedAt = helmtime.Now() - h.LastRun.Phase = release.HookPhaseFailed - return errors.Wrapf(err, "warning: Hook %s %s failed", hook, h.Path) - } - - // Watch hook resources until they have completed - err = cfg.KubeClient.WatchUntilReady(resources, timeout) - // Note the time of success/failure - h.LastRun.CompletedAt = helmtime.Now() - // Mark hook as succeeded or failed - if err != nil { - h.LastRun.Phase = release.HookPhaseFailed - // If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted - // under failed condition. If so, then clear the corresponding resource object in the hook - if err := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); err != nil { - return err - } - return err - } - h.LastRun.Phase = release.HookPhaseSucceeded - } - - // If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted - // under succeeded condition. If so, then clear the corresponding resource object in each hook - for _, h := range executingHooks { - if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil { - return err - } - } - - return nil -} - -// hookByWeight is a sorter for hooks -type hookByWeight []*release.Hook - -func (x hookByWeight) Len() int { return len(x) } -func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] } -func (x hookByWeight) Less(i, j int) bool { - if x[i].Weight == x[j].Weight { - return x[i].Name < x[j].Name - } - return x[i].Weight < x[j].Weight -} - -// deleteHookByPolicy deletes a hook if the hook policy instructs it to -func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error { - // Never delete CustomResourceDefinitions; this could cause lots of - // cascading garbage collection. - if h.Kind == "CustomResourceDefinition" { - return nil - } - if hookHasDeletePolicy(h, policy) { - resources, err := cfg.KubeClient.Build(bytes.NewBufferString(h.Manifest), false) - if err != nil { - return errors.Wrapf(err, "unable to build kubernetes object for deleting hook %s", h.Path) - } - _, errs := cfg.KubeClient.Delete(resources, kube.DeleteOptions{Wait: true}) - if len(errs) > 0 { - return errors.New(joinErrors(errs)) - } - - // wait for resources until they are deleted to avoid conflicts - if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok { - if err := kubeClient.WaitForDelete(resources, timeout); err != nil { - return err - } - } - } - return nil -} - -// hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices -// supported by helm. If so, mark the hook as one should be deleted. -func hookHasDeletePolicy(h *release.Hook, policy release.HookDeletePolicy) bool { - for _, v := range h.DeletePolicies { - if policy == v { - return true - } - } - return false -} - -func (cfg *Configuration) deleteHooks(hooks []*release.Hook) error { - var manifests []string - for _, h := range hooks { - manifests = append(manifests, h.Manifest) - } - - manifestsStr := strings.Join(manifests, "\n---\n") - resources, err := cfg.KubeClient.Build(bytes.NewBufferString(manifestsStr), false) - if err != nil { - return errors.Wrapf(err, "unable to build kubernetes objects for deleting hooks") - } - _, errs := cfg.KubeClient.Delete(resources, kube.DeleteOptions{Wait: true}) - if len(errs) > 0 { - return errors.New(joinErrors(errs)) - } - return nil -} diff --git a/pkg/helm/pkg/action/install.go b/pkg/helm/pkg/action/install.go index 819091f3..fff038c2 100644 --- a/pkg/helm/pkg/action/install.go +++ b/pkg/helm/pkg/action/install.go @@ -17,114 +17,38 @@ limitations under the License. package action import ( - "bytes" - "context" + "errors" "fmt" - "io" + "io/fs" "net/url" "os" - "path" "path/filepath" "strings" - "sync" - "text/template" - "time" - - "github.com/Masterminds/sprig/v3" - "github.com/pkg/errors" - v1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/cli-runtime/pkg/resource" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + + ci "github.com/werf/nelm/pkg/helm/pkg/chart" "github.com/werf/nelm/pkg/helm/pkg/cli" "github.com/werf/nelm/pkg/helm/pkg/downloader" "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/kube" - kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" - "github.com/werf/nelm/pkg/helm/pkg/phases" - "github.com/werf/nelm/pkg/helm/pkg/phases/phasemanagers" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" - "github.com/werf/nelm/pkg/helm/pkg/postrender" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/storage" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + ri "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) -// NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine -// but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually +// notesFileSuffix that we want to treat specially. It goes through the templating engine +// but it's not a YAML file (resource) hence can't have hooks, etc. And the user actually // wants to see this file after rendering in the status command. However, it must be a suffix // since there can be filepath in front of it. const notesFileSuffix = "NOTES.txt" const defaultDirectoryPermission = 0755 -// Install performs an installation operation. -type Install struct { - cfg *Configuration - - ChartPathOptions - - ClientOnly bool - Force bool - CreateNamespace bool - DryRun bool - DryRunOption string - DisableHooks bool - Replace bool - Wait bool - WaitForJobs bool - Devel bool - DependencyUpdate bool - Timeout time.Duration - Namespace string - ReleaseName string - GenerateName bool - NameTemplate string - Description string - OutputDir string - Atomic bool - SkipCRDs bool - SubNotes bool - DisableOpenAPIValidation bool - IncludeCRDs bool - Labels map[string]string - // KubeVersion allows specifying a custom kubernetes version to use and - // APIVersions allows a manual set of supported API Versions to be passed - // (for things like templating). These are ignored if ClientOnly is false - KubeVersion *chartutil.KubeVersion - APIVersions chartutil.VersionSet - // Used by helm template to render charts with .Release.IsUpgrade. Ignored if Dry-Run is false - IsUpgrade bool - // Enable DNS lookups when rendering templates - EnableDNS bool - // Used by helm template to add the release as part of OutputDir path - // OutputDir/ - UseReleaseName bool - PostRenderer postrender.PostRenderer - // Lock to control raceconditions when the process receives a SIGTERM - Lock sync.Mutex - - CleanupOnFail bool - StagesSplitter phases.Splitter - StagesExternalDepsGenerator phases.ExternalDepsGenerator - DeployReportPath string -} - // ChartPathOptions captures common options used for controlling chart paths type ChartPathOptions struct { CaFile string // --ca-file CertFile string // --cert-file KeyFile string // --key-file - InsecureSkipTLSverify bool // --insecure-skip-verify + InsecureSkipTLSVerify bool // --insecure-skip-verify PlainHTTP bool // --plain-http Keyring string // --keyring Password string // --password @@ -139,571 +63,30 @@ type ChartPathOptions struct { registryClient *registry.Client } -// NewInstall creates a new Install object with the given configuration. -func NewInstall(cfg *Configuration, stagesSplitter phases.Splitter, stagesExternalDepsGenerator phases.ExternalDepsGenerator) *Install { - if stagesSplitter == nil { - stagesSplitter = &phases.SingleStageSplitter{} - } - - if stagesExternalDepsGenerator == nil { - stagesExternalDepsGenerator = &phases.NoExternalDepsGenerator{} - } - - in := &Install{ - cfg: cfg, - - StagesSplitter: stagesSplitter, - StagesExternalDepsGenerator: stagesExternalDepsGenerator, - } - in.ChartPathOptions.registryClient = cfg.RegistryClient - - return in -} - -// SetRegistryClient sets the registry client for the install action -func (i *Install) SetRegistryClient(registryClient *registry.Client) { - i.ChartPathOptions.registryClient = registryClient -} - -// GetRegistryClient get the registry client. -func (i *Install) GetRegistryClient() *registry.Client { - return i.ChartPathOptions.registryClient -} - -func (i *Install) installCRDs(crds []chart.CRD) error { - // We do these one file at a time in the order they were read. - totalItems := []*resource.Info{} - for _, obj := range crds { - // Read in the resources - res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.File.Data), false) +func releaseListToV1List(ls []ri.Releaser) ([]*release.Release, error) { + rls := make([]*release.Release, 0, len(ls)) + for _, val := range ls { + rel, err := releaserToV1Release(val) if err != nil { - return errors.Wrapf(err, "failed to install CRD %s", obj.Name) - } - - // Send them to Kube - if _, err := i.cfg.KubeClient.Create(res, kube.CreateOptions{}); err != nil { - // If the error is CRD already exists, continue. - if apierrors.IsAlreadyExists(err) { - crdName := res[0].Name - i.cfg.Log("CRD %s is already present. Skipping.", crdName) - continue - } - return errors.Wrapf(err, "failed to install CRD %s", obj.Name) - } - totalItems = append(totalItems, res...) - } - if len(totalItems) > 0 { - // Give time for the CRD to be recognized. - if err := i.cfg.KubeClient.Wait(totalItems, 60*time.Second); err != nil { - return err - } - - // If we have already gathered the capabilities, we need to invalidate - // the cache so that the new CRDs are recognized. This should only be - // the case when an action configuration is reused for multiple actions, - // as otherwise it is later loaded by ourselves when getCapabilities - // is called later on in the installation process. - if i.cfg.Capabilities != nil { - discoveryClient, err := i.cfg.RESTClientGetter.ToDiscoveryClient() - if err != nil { - return err - } - - i.cfg.Log("Clearing discovery cache") - discoveryClient.Invalidate() - - _, _ = discoveryClient.ServerGroups() - } - - // Invalidate the REST mapper, since it will not have the new CRDs - // present. - restMapper, err := i.cfg.RESTClientGetter.ToRESTMapper() - if err != nil { - return err - } - if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok { - i.cfg.Log("Clearing REST mapper cache") - resettable.Reset() - } - } - return nil -} - -// Run executes the installation -// -// If DryRun is set to true, this will prepare the release, but not install it - -func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { - ctx := context.Background() - return i.RunWithContext(ctx, chrt, vals) -} - -// Run executes the installation with Context -// -// When the task is cancelled through ctx, the function returns and the install -// proceeds in the background. -func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { - // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) - if !i.ClientOnly { - if err := i.cfg.KubeClient.IsReachable(); err != nil { - return nil, err - } - } - - if err := i.availableName(); err != nil { - return nil, err - } - - if err := chartutil.ProcessDependenciesWithMerge(chrt, &vals); err != nil { - return nil, err - } - - var interactWithRemote bool - if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" { - interactWithRemote = true - } - - // Pre-install anything in the crd/ directory. We do this before Helm - // contacts the upstream server and builds the capabilities object. - if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 { - // On dry run, bail here - if i.isDryRun() { - i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") - } else if err := i.installCRDs(crds); err != nil { return nil, err } + rls = append(rls, rel) } - if i.ClientOnly { - // Add mock objects in here so it doesn't use Kube API server - // NOTE(bacongobbler): used for `helm template` - i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy() - if i.KubeVersion != nil { - i.cfg.Capabilities.KubeVersion = *i.KubeVersion - } - i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...) - i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard} - - mem := driver.NewMemory() - mem.SetNamespace(i.Namespace) - i.cfg.Releases = storage.Init(mem) - } else if !i.ClientOnly && len(i.APIVersions) > 0 { - i.cfg.Log("API Version list given outside of client only mode, this list will be ignored") - } - - // Make sure if Atomic is set, that wait is set as well. This makes it so - // the user doesn't have to specify both - i.Wait = i.Wait || i.Atomic - - caps, err := i.cfg.getCapabilities() - if err != nil { - return nil, err - } - - // special case for helm template --is-upgrade - isUpgrade := i.IsUpgrade && i.isDryRun() - options := chartutil.ReleaseOptions{ - Name: i.ReleaseName, - Namespace: i.Namespace, - Revision: 1, - IsInstall: !isUpgrade, - IsUpgrade: isUpgrade, - } - valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps, nil, nil) - if err != nil { - return nil, err - } - - if driver.ContainsSystemLabels(i.Labels) { - return nil, fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()) - } - - rel := i.createRelease(chrt, vals, i.Labels) - - if !i.isDryRun() && i.DeployReportPath != "" { - defer func() { - deployReportData, err := release.NewDeployReport().FromRelease(rel).ToJSONData() - if err != nil { - i.cfg.Log("warning: error creating deploy report data: %s", err) - return - } - - if err := os.WriteFile(i.DeployReportPath, deployReportData, 0o644); err != nil { - i.cfg.Log("warning: error writing deploy report file: %s", err) - return - } - }() - } - - var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, helmopts.HelmOptions{}) - // Even for errors, attach this if available - if manifestDoc != nil { - rel.Manifest = manifestDoc.String() - } - // Check error from render - if err != nil { - rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) - // Return a release with partial data so that the client can show debugging information. - return rel, err - } - - // Mark this release as in-progress - rel.SetStatus(release.StatusPendingInstall, "Initial install underway") - - var toBeAdopted kube.ResourceList - resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation) - if err != nil { - return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest") - } - - // It is safe to use "force" here because these are resources currently rendered by the chart. - err = resources.Visit(releaseutil.SetMetadataVisitor(rel.Name, rel.Namespace, true)) - if err != nil { - return nil, err - } - - // Install requires an extra validation step of checking that resources - // don't already exist before we actually create resources. If we continue - // forward and create the release object with resources that already exist, - // we'll end up in a state where we will delete those resources upon - // deleting the release because the manifest will be pointing at that - // resource - if !i.ClientOnly && !isUpgrade && len(resources) > 0 { - toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace) - if err != nil { - return nil, errors.Wrap(err, "Unable to continue with install") - } - } - - // Bail out here if it is a dry run - if i.isDryRun() { - rel.Info.Description = "Dry run complete" - return rel, nil - } - - if i.CreateNamespace { - ns := &v1.Namespace{ - TypeMeta: metav1.TypeMeta{ - APIVersion: "v1", - Kind: "Namespace", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: i.Namespace, - Labels: map[string]string{ - "name": i.Namespace, - }, - }, - } - buf, err := yaml.Marshal(ns) - if err != nil { - return nil, err - } - resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true) - if err != nil { - return nil, err - } - if _, err := i.cfg.KubeClient.Create(resourceList, kube.CreateOptions{ - SkipIfAlreadyExists: true, - }); err != nil && !apierrors.IsAlreadyExists(err) { - return nil, err - } - } - - // If Replace is true, we need to supercede the last release. - if i.Replace { - if err := i.replaceRelease(rel); err != nil { - return nil, err - } - } - - // Store the release in history before continuing (new in Helm 3). We always know - // that this is a create operation. - if err := i.cfg.Releases.Create(rel); err != nil { - // We could try to recover gracefully here, but since nothing has been installed - // yet, this is probably safer than trying to continue when we know storage is - // not working. - return rel, err - } - - var createdToCleanup kube.ResourceList - rel, createdToCleanup, err = i.performInstallCtx(ctx, rel, toBeAdopted, resources) - if err != nil { - rel, err = i.failRelease(rel, createdToCleanup, err) - } - return rel, err -} - -func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, kube.ResourceList, error) { - type Msg struct { - r *release.Release - e error - - createdToCleanup kube.ResourceList - } - resultChan := make(chan Msg, 1) - - go func() { - rel, createdToCleanup, err := i.performInstall(rel, toBeAdopted, resources) - resultChan <- Msg{rel, err, createdToCleanup} - }() - select { - case <-ctx.Done(): - err := ctx.Err() - return rel, nil, err - case msg := <-resultChan: - return msg.r, msg.createdToCleanup, msg.e - } -} - -// isDryRun returns true if Upgrade is set to run as a DryRun -func (i *Install) isDryRun() bool { - if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" { - return true - } - return false -} - -func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, kube.ResourceList, error) { - var err error - // pre-install hooks - if !i.DisableHooks { - if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil { - return rel, nil, fmt.Errorf("failed pre-install: %s", err) - } - } - - history, err := i.cfg.Releases.HistoryUntilRevision(rel.Name, rel.Version) - if err != nil { - return rel, nil, fmt.Errorf("error getting release history: %w", err) - } - - rolloutPhase, err := phases.NewRolloutPhase(rel, i.StagesSplitter, i.cfg.KubeClient). - ParseStages(resources) - if err != nil { - return rel, nil, fmt.Errorf("error parsing stages for rollout phase: %w", err) - } - - if err := rolloutPhase.GenerateStagesExternalDeps(i.StagesExternalDepsGenerator); err != nil { - return rel, nil, fmt.Errorf("error generating external deps for rollout phase: %w", err) - } - - deployedResourcesCalculator := phases.NewDeployedResourcesCalculator(history, i.StagesSplitter, i.cfg.KubeClient) - - rolloutPhaseManager, err := phasemanagers.NewRolloutPhaseManager(rolloutPhase, deployedResourcesCalculator, rel, i.cfg.Releases, i.cfg.KubeClient). - AddPreviouslyDeployedResources(toBeAdopted). - AddCalculatedPreviouslyDeployedResources() - if err != nil { - return rel, nil, fmt.Errorf("error calculating previously deployed resources for rollout phase manager: %w", err) - } - - if err := rolloutPhaseManager.DoStage( - func(stgIndex int, stage *stages.Stage) error { - if len(stage.ExternalDependencies) == 0 || !i.Wait { - return nil - } - - if i.WaitForJobs { - return i.cfg.KubeClient.WaitWithJobs(stage.ExternalDependencies.AsResourceList(), i.Timeout) - } else { - return i.cfg.KubeClient.Wait(stage.ExternalDependencies.AsResourceList(), i.Timeout) - } - }, - func(stgIndex int, stage *stages.Stage, prevDeployedStgResources kube.ResourceList) error { - // At this point, we can do the install. Note that before we were detecting whether to - // do an update, but it's not clear whether we WANT to do an update if the re-use is set - // to true, since that is basically an upgrade operation. - if len(prevDeployedStgResources) == 0 && len(stage.DesiredResources) > 0 { - stage.Result, err = i.cfg.KubeClient.Create(stage.DesiredResources, kube.CreateOptions{}) - if err != nil { - return err - } - } else if len(stage.DesiredResources) > 0 { - stage.Result, err = i.cfg.KubeClient.Update(prevDeployedStgResources, stage.DesiredResources, i.Force, kube.UpdateOptions{ - SkipDeleteIfInvalidOwnership: true, - ReleaseName: rel.Name, - ReleaseNamespace: rel.Namespace, - }) - if err != nil { - return err - } - } - - return nil - }, - func(stgIndex int, stage *stages.Stage) error { - if !i.Wait { - return nil - } - - if i.WaitForJobs { - return i.cfg.KubeClient.WaitWithJobs(stage.DesiredResources, i.Timeout) - } else { - return i.cfg.KubeClient.Wait(stage.DesiredResources, i.Timeout) - } - }, - ); err != nil { - createdResourcesToDelete := kube.ResourceList{} - var applyErr *phasemanagers.ApplyError - if errors.As(err, &applyErr) { - createdResourcesToDelete = rolloutPhaseManager.Phase.SortedStages[applyErr.StageIndex].Result.Created - } - - return rel, createdResourcesToDelete, fmt.Errorf("error processing rollout phase stage: %w", err) - } - - if err := rolloutPhaseManager.DeleteOrphanedResources(); err != nil { - i.cfg.Log("failure removing resources no longer present in the release: %w", err) - } - - if !i.DisableHooks { - if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil { - return rel, nil, fmt.Errorf("failed post-install: %s", err) - } - } - - if len(i.Description) > 0 { - rel.SetStatus(release.StatusDeployed, i.Description) - } else { - rel.SetStatus(release.StatusDeployed, "Install complete") - } - - // This is a tricky case. The release has been created, but the result - // cannot be recorded. The truest thing to tell the user is that the - // release was created. However, the user will not be able to do anything - // further with this release. - // - // One possible strategy would be to do a timed retry to see if we can get - // this stored in the future. - if err := i.recordRelease(rel); err != nil { - i.cfg.Log("failed to record the release: %s", err) - } - - return rel, nil, nil -} - -func (i *Install) failRelease(rel *release.Release, createdToCleanup kube.ResourceList, err error) (*release.Release, error) { - rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) - - if i.CleanupOnFail && len(createdToCleanup) > 0 { - i.cfg.Log("Cleanup on fail set, cleaning up %d resources", len(createdToCleanup)) - _, errs := i.cfg.KubeClient.Delete(createdToCleanup, kube.DeleteOptions{ - Wait: true, - WaitTimeout: i.Timeout, - SkipIfInvalidOwnership: true, - ReleaseName: rel.Name, - ReleaseNamespace: rel.Namespace, - }) - if errs != nil { - var errorList []string - for _, e := range errs { - errorList = append(errorList, e.Error()) - } - return rel, errors.Wrapf(fmt.Errorf("unable to cleanup resources: %s", strings.Join(errorList, ", ")), "an error occurred while cleaning up resources. original install error: %s", err) - } - i.cfg.Log("Resource cleanup complete") - } - - if i.Atomic { - i.cfg.Log("Install failed and atomic is set, uninstalling release") - uninstall := NewUninstall(i.cfg, i.StagesSplitter) - uninstall.DisableHooks = i.DisableHooks - uninstall.KeepHistory = false - uninstall.Timeout = i.Timeout - if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil { - return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err) - } - return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName) - } - i.recordRelease(rel) // Ignore the error, since we have another error to deal with. - return rel, err + return rls, nil } -// availableName tests whether a name is available -// -// Roughly, this will return an error if name is -// -// - empty -// - too long -// - already in use, and not deleted -// - used by a deleted release, and i.Replace is false -func (i *Install) availableName() error { - start := i.ReleaseName - - if err := chartutil.ValidateReleaseName(start); err != nil { - return errors.Wrapf(err, "release name %q", start) - } - // On dry run, bail here - if i.isDryRun() { - return nil - } - - h, err := i.cfg.Releases.History(start) - if err != nil || len(h) < 1 { - return nil +func releaseV1ListToReleaserList(ls []*release.Release) ([]ri.Releaser, error) { + rls := make([]ri.Releaser, 0, len(ls)) + for _, val := range ls { + rls = append(rls, val) } - releaseutil.Reverse(h, releaseutil.SortByRevision) - rel := h[0] - if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) { - return nil - } - return errors.New("cannot re-use a name that is still in use") -} - -// createRelease creates a new release object -func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release { - ts := i.cfg.Now() - return release.SetInitPhaseStageInfo(&release.Release{ - Name: i.ReleaseName, - Namespace: i.Namespace, - Chart: chrt, - Config: rawVals, - Info: &release.Info{ - FirstDeployed: ts, - LastDeployed: ts, - Status: release.StatusUnknown, - }, - Version: 1, - Labels: labels, - }) + return rls, nil } -// recordRelease with an update operation in case reuse has been set. -func (i *Install) recordRelease(r *release.Release) error { - // This is a legacy function which has been reduced to a oneliner. Could probably - // refactor it out. - return i.cfg.Releases.Update(r) -} - -// replaceRelease replaces an older release with this one -// -// This allows us to re-use names by superseding an existing release with a new one -func (i *Install) replaceRelease(rel *release.Release) error { - hist, err := i.cfg.Releases.History(rel.Name) - if err != nil || len(hist) == 0 { - // No releases exist for this name, so we can return early - return nil - } - - releaseutil.Reverse(hist, releaseutil.SortByRevision) - last := hist[0] - - // Update version to the next available - rel.Version = last.Version + 1 - - // Do not change the status of a failed release. - if last.Info.Status == release.StatusFailed { - return nil - } - - // For any other status, mark it as superseded and store the old record - last.SetStatus(release.StatusSuperseded, "superseded by new release") - return i.recordRelease(last) -} - -// write the to /. controls if the file is created or content will be appended -func writeToFile(outputDir string, name string, data string, append bool) error { +// write the to /. controls if the file is created or content will be appended +func writeToFile(outputDir string, name string, data string, appendData bool) error { outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) err := ensureDirectoryForFile(outfileName) @@ -711,14 +94,14 @@ func writeToFile(outputDir string, name string, data string, append bool) error return err } - f, err := createOrOpenFile(outfileName, append) + f, err := createOrOpenFile(outfileName, appendData) if err != nil { return err } defer f.Close() - _, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data)) + _, err = fmt.Fprintf(f, "---\n# Source: %s\n%s\n", name, data) if err != nil { return err @@ -728,115 +111,82 @@ func writeToFile(outputDir string, name string, data string, append bool) error return nil } -func createOrOpenFile(filename string, append bool) (*os.File, error) { - if append { +func createOrOpenFile(filename string, appendData bool) (*os.File, error) { + if appendData { return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) } return os.Create(filename) } -// check if the directory exists to create file. creates if don't exists +// check if the directory exists to create file. creates if doesn't exist func ensureDirectoryForFile(file string) error { - baseDir := path.Dir(file) + baseDir := filepath.Dir(file) _, err := os.Stat(baseDir) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } return os.MkdirAll(baseDir, defaultDirectoryPermission) } -// NameAndChart returns the name and chart that should be used. -// -// This will read the flags and handle name generation if necessary. -func (i *Install) NameAndChart(args []string) (string, string, error) { - flagsNotSet := func() error { - if i.GenerateName { - return errors.New("cannot set --generate-name and also specify a name") - } - if i.NameTemplate != "" { - return errors.New("cannot set --name-template and also specify a name") - } - return nil - } - - if len(args) > 2 { - return args[0], args[1], errors.Errorf("expected at most two arguments, unexpected arguments: %v", strings.Join(args[2:], ", ")) - } - - if len(args) == 2 { - return args[0], args[1], flagsNotSet() - } - - if i.NameTemplate != "" { - name, err := TemplateName(i.NameTemplate) - return name, args[0], err - } - - if i.ReleaseName != "" { - return i.ReleaseName, args[0], nil - } - - if !i.GenerateName { - return "", args[0], errors.New("must either provide a name or specify --generate-name") - } - - base := filepath.Base(args[0]) - if base == "." || base == "" { - base = "chart" - } - // if present, strip out the file extension from the name - if idx := strings.Index(base, "."); idx != -1 { - base = base[0:idx] - } - - return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil -} - -// TemplateName renders a name template, returning the name or an error. -func TemplateName(nameTemplate string) (string, error) { - if nameTemplate == "" { - return "", nil - } - - t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) +// CheckDependencies checks the dependencies for a chart. +func CheckDependencies(ch ci.Charter, reqs []ci.Dependency) error { + ac, err := ci.NewAccessor(ch) if err != nil { - return "", err - } - var b bytes.Buffer - if err := t.Execute(&b, nil); err != nil { - return "", err + return err } - return b.String(), nil -} - -// CheckDependencies checks the dependencies for a chart. -func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { var missing []string OUTER: for _, r := range reqs { - for _, d := range ch.Dependencies() { - if d.Name() == r.Name { + rac, err := ci.NewDependencyAccessor(r) + if err != nil { + return err + } + for _, d := range ac.Dependencies() { + dac, err := ci.NewAccessor(d) + if err != nil { + return err + } + if dac.Name() == rac.Name() { continue OUTER } } - missing = append(missing, r.Name) + missing = append(missing, rac.Name()) } if len(missing) > 0 { - return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) + return fmt.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) } return nil } +func portOrDefault(u *url.URL) string { + if p := u.Port(); p != "" { + return p + } + + switch u.Scheme { + case "http": + return "80" + case "https": + return "443" + default: + return "" + } +} + +func urlEqual(u1, u2 *url.URL) bool { + return u1.Scheme == u2.Scheme && u1.Hostname() == u2.Hostname() && portOrDefault(u1) == portOrDefault(u2) +} + // LocateChart looks for a chart directory in known places, and returns either the full path or an error. // // This does not ensure that the chart is well-formed; only that the requested filename exists. // // Order of resolution: -// - relative to current working directory +// - relative to current working directory when --repo flag is not presented // - if path is absolute or begins with '.', error out here // - URL // @@ -849,20 +199,22 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( name = strings.TrimSpace(name) version := strings.TrimSpace(c.Version) - if _, err := os.Stat(name); err == nil { - abs, err := filepath.Abs(name) - if err != nil { - return abs, err - } - if c.Verify { - if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil { - return "", err + if c.RepoURL == "" { + if _, err := os.Stat(name); err == nil { + abs, err := filepath.Abs(name) + if err != nil { + return abs, err + } + if c.Verify { + if _, err := downloader.VerifyChart(abs, abs+".prov", c.Keyring); err != nil { + return "", err + } } + return abs, nil + } + if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { + return name, fmt.Errorf("path %q not found", name) } - return abs, nil - } - if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { - return name, errors.Errorf("path %q not found", name) } dl := downloader.ChartDownloader{ @@ -872,11 +224,13 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( Options: []getter.Option{ getter.WithPassCredentialsAll(c.PassCredentialsAll), getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), - getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify), + getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSVerify), getter.WithPlainHTTP(c.PlainHTTP), + getter.WithBasicAuth(c.Username, c.Password), }, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, + ContentCache: settings.ContentCache, RegistryClient: c.registryClient, } @@ -888,8 +242,16 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( dl.Verify = downloader.VerifyAlways } if c.RepoURL != "" { - chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(c.RepoURL, c.Username, c.Password, name, version, - c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, c.PassCredentialsAll, getter.All(settings)) + chartURL, err := repo.FindChartInRepoURL( + c.RepoURL, + name, + getter.All(settings), + repo.WithChartVersion(version), + repo.WithClientTLS(c.CertFile, c.KeyFile, c.CaFile), + repo.WithUsernamePassword(c.Username, c.Password), + repo.WithInsecureSkipTLSVerify(c.InsecureSkipTLSVerify), + repo.WithPassCredentialsAll(c.PassCredentialsAll), + ) if err != nil { return "", err } @@ -909,7 +271,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( // Host on URL (returned from url.Parse) contains the port if present. // This check ensures credentials are not passed between different // services on different ports. - if c.PassCredentialsAll || (u1.Scheme == u2.Scheme && u1.Host == u2.Host) { + if c.PassCredentialsAll || urlEqual(u1, u2) { dl.Options = append(dl.Options, getter.WithBasicAuth(c.Username, c.Password)) } else { dl.Options = append(dl.Options, getter.WithBasicAuth("", "")) @@ -922,7 +284,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( return "", err } - filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache) + filename, _, err := dl.DownloadToCache(name, version) if err != nil { return "", err } @@ -933,7 +295,3 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( } return lname, nil } - -func (c *ChartPathOptions) SetRegistryClient(cli *registry.Client) { - c.registryClient = cli -} diff --git a/pkg/helm/pkg/action/install_test.go b/pkg/helm/pkg/action/install_test.go deleted file mode 100644 index 9d726cc3..00000000 --- a/pkg/helm/pkg/action/install_test.go +++ /dev/null @@ -1,763 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "context" - "fmt" - "io" - "os" - "path/filepath" - "regexp" - "runtime" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/werf/nelm/pkg/helm/intern/test" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" -) - -type nameTemplateTestCase struct { - tpl string - expected string - expectedErrorStr string -} - -func installAction(t *testing.T) *Install { - config := actionConfigFixture(t) - instAction := NewInstall(config, nil, nil) - instAction.Namespace = "spaced" - instAction.ReleaseName = "test-install-release" - - return instAction -} - -func TestInstallRelease(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - instAction := installAction(t) - vals := map[string]interface{}{} - ctx, done := context.WithCancel(context.Background()) - res, err := instAction.RunWithContext(ctx, buildChart(), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - is.Equal(res.Name, "test-install-release", "Expected release name.") - is.Equal(res.Namespace, "spaced") - - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.NoError(err) - - is.Len(rel.Hooks, 1) - is.Equal(rel.Hooks[0].Manifest, manifestWithHook) - is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) - is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") - - is.NotEqual(len(res.Manifest), 0) - is.NotEqual(len(rel.Manifest), 0) - is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") - is.Equal(rel.Info.Description, "Install complete") - - // Detecting previous bug where context termination after successful release - // caused release to fail. - done() - time.Sleep(time.Millisecond * 100) - lastRelease, err := instAction.cfg.Releases.Last(rel.Name) - req.NoError(err) - is.Equal(lastRelease.Info.Status, release.StatusDeployed) -} - -func TestInstallReleaseWithValues(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - userVals := map[string]interface{}{ - "nestedKey": map[string]interface{}{ - "simpleKey": "simpleValue", - }, - } - expectedUserValues := map[string]interface{}{ - "nestedKey": map[string]interface{}{ - "simpleKey": "simpleValue", - }, - } - res, err := instAction.Run(buildChart(withSampleValues()), userVals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - is.Equal(res.Name, "test-install-release", "Expected release name.") - is.Equal(res.Namespace, "spaced") - - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.NoError(err) - - is.Len(rel.Hooks, 1) - is.Equal(rel.Hooks[0].Manifest, manifestWithHook) - is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) - is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") - - is.NotEqual(len(res.Manifest), 0) - is.NotEqual(len(rel.Manifest), 0) - is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") - is.Equal("Install complete", rel.Info.Description) - is.Equal(expectedUserValues, rel.Config) -} - -func TestInstallReleaseClientOnly(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ClientOnly = true - instAction.Run(buildChart(), nil) // disregard output - - is.Equal(instAction.cfg.Capabilities, chartutil.DefaultCapabilities) - is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard}) -} - -func TestInstallRelease_NoName(t *testing.T) { - instAction := installAction(t) - instAction.ReleaseName = "" - vals := map[string]interface{}{} - _, err := instAction.Run(buildChart(), vals) - if err == nil { - t.Fatal("expected failure when no name is specified") - } - assert.Contains(t, err.Error(), "no name provided") -} - -func TestInstallRelease_WithNotes(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "with-notes" - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("note here")), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - is.Equal(res.Name, "with-notes") - is.Equal(res.Namespace, "spaced") - - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.NoError(err) - is.Len(rel.Hooks, 1) - is.Equal(rel.Hooks[0].Manifest, manifestWithHook) - is.Equal(rel.Hooks[0].Events[0], release.HookPostInstall) - is.Equal(rel.Hooks[0].Events[1], release.HookPreDelete, "Expected event 0 is pre-delete") - is.NotEqual(len(res.Manifest), 0) - is.NotEqual(len(rel.Manifest), 0) - is.Contains(rel.Manifest, "---\n# Source: hello/templates/hello\nhello: world") - is.Equal(rel.Info.Description, "Install complete") - - is.Equal(rel.Info.Notes, "note here") -} - -func TestInstallRelease_WithNotesRendered(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "with-notes" - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.NoError(err) - - expectedNotes := fmt.Sprintf("got-%s", res.Name) - is.Equal(expectedNotes, rel.Info.Notes) - is.Equal(rel.Info.Description, "Install complete") -} - -func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) { - // Regression: Make sure that the child's notes don't override the parent's - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "with-notes" - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.Equal("with-notes", rel.Name) - is.NoError(err) - is.Equal("parent", rel.Info.Notes) - is.Equal(rel.Info.Description, "Install complete") -} - -func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { - // Regression: Make sure that the child's notes don't override the parent's - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "with-notes" - instAction.SubNotes = true - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.Equal("with-notes", rel.Name) - is.NoError(err) - // test run can return as either 'parent\nchild' or 'child\nparent' - if !strings.Contains(rel.Info.Notes, "parent") && !strings.Contains(rel.Info.Notes, "child") { - t.Fatalf("Expected 'parent\nchild' or 'child\nparent', got '%s'", rel.Info.Notes) - } - is.Equal(rel.Info.Description, "Install complete") -} - -func TestInstallRelease_DryRun(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.DryRun = true - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withSampleTemplates()), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world") - is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") - is.Contains(res.Manifest, "hello: Earth") - is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}") - is.NotContains(res.Manifest, "empty") - - _, err = instAction.cfg.Releases.Get(res.Name, res.Version) - is.Error(err) - is.Len(res.Hooks, 1) - is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run") - is.Equal(res.Info.Description, "Dry run complete") -} - -// Regression test for #7955 -func TestInstallRelease_DryRun_Lookup(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.DryRun = true - vals := map[string]interface{}{} - - mockChart := buildChart(withSampleTemplates()) - mockChart.Templates = append(mockChart.Templates, &chart.File{ - Name: "templates/lookup", - Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), - }) - - res, err := instAction.Run(mockChart, vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - is.Contains(res.Manifest, "goodbye: map[]") -} - -func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.DryRun = true - vals := map[string]interface{}{} - _, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals) - expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh" - if err == nil { - t.Fatalf("Install should fail containing error: %s", expectedErr) - } - if err != nil { - is.Contains(err.Error(), expectedErr) - } -} - -func TestInstallRelease_NoHooks(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.DisableHooks = true - instAction.ReleaseName = "no-hooks" - instAction.cfg.Releases.Create(releaseStub()) - - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks") -} - -func TestInstallRelease_FailedHooks(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "failed-hooks" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WatchUntilReadyError = fmt.Errorf("Failed watch") - instAction.cfg.KubeClient = failer - - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(res.Info.Description, "failed post-install") - is.Equal(release.StatusFailed, res.Info.Status) -} - -func TestInstallRelease_ReplaceRelease(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.Replace = true - - rel := releaseStub() - rel.Info.Status = release.StatusUninstalled - instAction.cfg.Releases.Create(rel) - instAction.ReleaseName = rel.Name - - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) - is.NoError(err) - - // This should have been auto-incremented - is.Equal(2, res.Version) - is.Equal(res.Name, rel.Name) - - getres, err := instAction.cfg.Releases.Get(rel.Name, res.Version) - is.NoError(err) - is.Equal(getres.Info.Status, release.StatusDeployed) -} - -func TestInstallRelease_KubeVersion(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - vals := map[string]interface{}{} - _, err := instAction.Run(buildChart(withKube(">=0.0.0")), vals) - is.NoError(err) - - // This should fail for a few hundred years - instAction.ReleaseName = "should-fail" - vals = map[string]interface{}{} - _, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals) - is.Error(err) - is.Contains(err.Error(), "chart requires kubeVersion") -} - -func TestInstallRelease_Wait(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "come-fail-away" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - instAction.cfg.KubeClient = failer - instAction.Wait = true - vals := map[string]interface{}{} - - goroutines := runtime.NumGoroutine() - - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) - - is.Equal(goroutines, runtime.NumGoroutine()) -} -func TestInstallRelease_Wait_Interrupted(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "interrupted-release" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 10 * time.Second - instAction.cfg.KubeClient = failer - instAction.Wait = true - vals := map[string]interface{}{} - - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - time.AfterFunc(time.Second, cancel) - - goroutines := runtime.NumGoroutine() - - res, err := instAction.RunWithContext(ctx, buildChart(), vals) - is.Error(err) - is.Contains(res.Info.Description, "Release \"interrupted-release\" failed: context canceled") - is.Equal(res.Info.Status, release.StatusFailed) - - is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background - time.Sleep(10 * time.Second) // wait for goroutine to finish - is.Equal(goroutines, runtime.NumGoroutine()) -} -func TestInstallRelease_WaitForJobs(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "come-fail-away" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - instAction.cfg.KubeClient = failer - instAction.Wait = true - instAction.WaitForJobs = true - vals := map[string]interface{}{} - - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} - -func TestInstallRelease_Atomic(t *testing.T) { - is := assert.New(t) - - t.Run("atomic uninstall succeeds", func(t *testing.T) { - instAction := installAction(t) - instAction.ReleaseName = "come-fail-away" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - instAction.cfg.KubeClient = failer - instAction.Atomic = true - // disabling hooks to avoid an early fail when the - // the WaitForDelete is called on the pre-delete hook execution - instAction.DisableHooks = true - vals := map[string]interface{}{} - - res, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(err.Error(), "I timed out") - is.Contains(err.Error(), "atomic") - - // Now make sure it isn't in storage any more - _, err = instAction.cfg.Releases.Get(res.Name, res.Version) - is.Error(err) - is.Equal(err, driver.ErrReleaseNotFound) - }) - - t.Run("atomic uninstall fails", func(t *testing.T) { - instAction := installAction(t) - instAction.ReleaseName = "come-fail-away-with-me" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - failer.DeleteError = fmt.Errorf("uninstall fail") - instAction.cfg.KubeClient = failer - instAction.Atomic = true - vals := map[string]interface{}{} - - _, err := instAction.Run(buildChart(), vals) - is.Error(err) - is.Contains(err.Error(), "I timed out") - is.Contains(err.Error(), "uninstall fail") - is.Contains(err.Error(), "an error occurred while uninstalling the release") - }) -} -func TestInstallRelease_Atomic_Interrupted(t *testing.T) { - - is := assert.New(t) - instAction := installAction(t) - instAction.ReleaseName = "interrupted-release" - failer := instAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 10 * time.Second - instAction.cfg.KubeClient = failer - instAction.Atomic = true - vals := map[string]interface{}{} - - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - time.AfterFunc(time.Second, cancel) - - res, err := instAction.RunWithContext(ctx, buildChart(), vals) - is.Error(err) - is.Contains(err.Error(), "context canceled") - is.Contains(err.Error(), "atomic") - is.Contains(err.Error(), "uninstalled") - - // Now make sure it isn't in storage any more - _, err = instAction.cfg.Releases.Get(res.Name, res.Version) - is.Error(err) - is.Equal(err, driver.ErrReleaseNotFound) - -} -func TestNameTemplate(t *testing.T) { - testCases := []nameTemplateTestCase{ - // Just a straight up nop please - { - tpl: "foobar", - expected: "foobar", - expectedErrorStr: "", - }, - // Random numbers at the end for fun & profit - { - tpl: "foobar-{{randNumeric 6}}", - expected: "foobar-[0-9]{6}$", - expectedErrorStr: "", - }, - // Random numbers in the middle for fun & profit - { - tpl: "foobar-{{randNumeric 4}}-baz", - expected: "foobar-[0-9]{4}-baz$", - expectedErrorStr: "", - }, - // No such function - { - tpl: "foobar-{{randInteger}}", - expected: "", - expectedErrorStr: "function \"randInteger\" not defined", - }, - // Invalid template - { - tpl: "foobar-{{", - expected: "", - expectedErrorStr: "template: name-template:1: unclosed action", - }, - } - - for _, tc := range testCases { - - n, err := TemplateName(tc.tpl) - if err != nil { - if tc.expectedErrorStr == "" { - t.Errorf("Was not expecting error, but got: %v", err) - continue - } - re, compErr := regexp.Compile(tc.expectedErrorStr) - if compErr != nil { - t.Errorf("Expected error string failed to compile: %v", compErr) - continue - } - if !re.MatchString(err.Error()) { - t.Errorf("Error didn't match for %s expected %s but got %v", tc.tpl, tc.expectedErrorStr, err) - continue - } - } - if err == nil && tc.expectedErrorStr != "" { - t.Errorf("Was expecting error %s but didn't get an error back", tc.expectedErrorStr) - } - - if tc.expected != "" { - re, err := regexp.Compile(tc.expected) - if err != nil { - t.Errorf("Expected string failed to compile: %v", err) - continue - } - if !re.MatchString(n) { - t.Errorf("Returned name didn't match for %s expected %s but got %s", tc.tpl, tc.expected, n) - } - } - } -} - -func TestInstallReleaseOutputDir(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - vals := map[string]interface{}{} - - dir := t.TempDir() - - instAction.OutputDir = dir - - _, err := instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - _, err = os.Stat(filepath.Join(dir, "hello/templates/goodbye")) - is.NoError(err) - - _, err = os.Stat(filepath.Join(dir, "hello/templates/hello")) - is.NoError(err) - - _, err = os.Stat(filepath.Join(dir, "hello/templates/with-partials")) - is.NoError(err) - - _, err = os.Stat(filepath.Join(dir, "hello/templates/rbac")) - is.NoError(err) - - test.AssertGoldenFile(t, filepath.Join(dir, "hello/templates/rbac"), "rbac.txt") - - _, err = os.Stat(filepath.Join(dir, "hello/templates/empty")) - is.True(os.IsNotExist(err)) -} - -func TestInstallOutputDirWithReleaseName(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - vals := map[string]interface{}{} - - dir := t.TempDir() - - instAction.OutputDir = dir - instAction.UseReleaseName = true - instAction.ReleaseName = "madra" - - newDir := filepath.Join(dir, instAction.ReleaseName) - - _, err := instAction.Run(buildChart(withSampleTemplates(), withMultipleManifestTemplate()), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - _, err = os.Stat(filepath.Join(newDir, "hello/templates/goodbye")) - is.NoError(err) - - _, err = os.Stat(filepath.Join(newDir, "hello/templates/hello")) - is.NoError(err) - - _, err = os.Stat(filepath.Join(newDir, "hello/templates/with-partials")) - is.NoError(err) - - _, err = os.Stat(filepath.Join(newDir, "hello/templates/rbac")) - is.NoError(err) - - test.AssertGoldenFile(t, filepath.Join(newDir, "hello/templates/rbac"), "rbac.txt") - - _, err = os.Stat(filepath.Join(newDir, "hello/templates/empty")) - is.True(os.IsNotExist(err)) -} - -func TestNameAndChart(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - chartName := "./foo" - - name, chrt, err := instAction.NameAndChart([]string{chartName}) - if err != nil { - t.Fatal(err) - } - is.Equal(instAction.ReleaseName, name) - is.Equal(chartName, chrt) - - instAction.GenerateName = true - _, _, err = instAction.NameAndChart([]string{"foo", chartName}) - if err == nil { - t.Fatal("expected an error") - } - is.Equal("cannot set --generate-name and also specify a name", err.Error()) - - instAction.GenerateName = false - instAction.NameTemplate = "{{ . }}" - _, _, err = instAction.NameAndChart([]string{"foo", chartName}) - if err == nil { - t.Fatal("expected an error") - } - is.Equal("cannot set --name-template and also specify a name", err.Error()) - - instAction.NameTemplate = "" - instAction.ReleaseName = "" - _, _, err = instAction.NameAndChart([]string{chartName}) - if err == nil { - t.Fatal("expected an error") - } - is.Equal("must either provide a name or specify --generate-name", err.Error()) - - instAction.NameTemplate = "" - instAction.ReleaseName = "" - _, _, err = instAction.NameAndChart([]string{"foo", chartName, "bar"}) - if err == nil { - t.Fatal("expected an error") - } - is.Equal("expected at most two arguments, unexpected arguments: bar", err.Error()) -} - -func TestNameAndChartGenerateName(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - - instAction.ReleaseName = "" - instAction.GenerateName = true - - tests := []struct { - Name string - Chart string - ExpectedName string - }{ - { - "local filepath", - "./chart", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), - }, - { - "dot filepath", - ".", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), - }, - { - "empty filepath", - "", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), - }, - { - "packaged chart", - "chart.tgz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), - }, - { - "packaged chart with .tar.gz extension", - "chart.tar.gz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), - }, - { - "packaged chart with local extension", - "./chart.tgz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), - }, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.Name, func(t *testing.T) { - t.Parallel() - - name, chrt, err := instAction.NameAndChart([]string{tc.Chart}) - if err != nil { - t.Fatal(err) - } - - is.Equal(tc.ExpectedName, name) - is.Equal(tc.Chart, chrt) - }) - } -} - -func TestInstallWithLabels(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.Labels = map[string]string{ - "key1": "val1", - "key2": "val2", - } - res, err := instAction.Run(buildChart(), nil) - if err != nil { - t.Fatalf("Failed install: %s", err) - } - - is.Equal(instAction.Labels, res.Labels) -} - -func TestInstallWithSystemLabels(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.Labels = map[string]string{ - "owner": "val1", - "key2": "val2", - } - _, err := instAction.Run(buildChart(), nil) - if err == nil { - t.Fatal("expected an error") - } - - is.Equal(fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()), err) -} diff --git a/pkg/helm/pkg/action/lint.go b/pkg/helm/pkg/action/lint.go deleted file mode 100644 index 94fd940f..00000000 --- a/pkg/helm/pkg/action/lint.go +++ /dev/null @@ -1,132 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -// Lint is the action for checking that the semantics of a chart are well-formed. -// -// It provides the implementation of 'helm lint'. -type Lint struct { - Strict bool - Namespace string - WithSubcharts bool - Quiet bool - KubeVersion *chartutil.KubeVersion -} - -// LintResult is the result of Lint -type LintResult struct { - TotalChartsLinted int - Messages []support.Message - Errors []error -} - -// NewLint creates a new Lint object with the given configuration. -func NewLint() *Lint { - return &Lint{} -} - -// Run executes 'helm Lint' against the given chart. -func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult { - lowestTolerance := support.ErrorSev - if l.Strict { - lowestTolerance = support.WarningSev - } - result := &LintResult{} - for _, path := range paths { - linter, err := lintChart(path, vals, l.Namespace, l.KubeVersion) - if err != nil { - result.Errors = append(result.Errors, err) - continue - } - - result.Messages = append(result.Messages, linter.Messages...) - result.TotalChartsLinted++ - for _, msg := range linter.Messages { - if msg.Severity >= lowestTolerance { - result.Errors = append(result.Errors, msg.Err) - } - } - } - return result -} - -// HasWarningsOrErrors checks is LintResult has any warnings or errors -func HasWarningsOrErrors(result *LintResult) bool { - for _, msg := range result.Messages { - if msg.Severity > support.InfoSev { - return true - } - } - return len(result.Errors) > 0 -} - -func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) (support.Linter, error) { - var chartPath string - linter := support.Linter{} - - if strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") { - tempDir, err := os.MkdirTemp("", "helm-lint") - if err != nil { - return linter, errors.Wrap(err, "unable to create temp dir to extract tarball") - } - defer os.RemoveAll(tempDir) - - file, err := os.Open(path) - if err != nil { - return linter, errors.Wrap(err, "unable to open tarball") - } - defer file.Close() - - if err = chartutil.Expand(tempDir, file); err != nil { - return linter, errors.Wrap(err, "unable to extract tarball") - } - - files, err := os.ReadDir(tempDir) - if err != nil { - return linter, errors.Wrapf(err, "unable to read temporary output directory %s", tempDir) - } - if !files[0].IsDir() { - return linter, errors.Errorf("unexpected file %s in temporary output directory %s", files[0].Name(), tempDir) - } - - chartPath = filepath.Join(tempDir, files[0].Name()) - } else { - chartPath = path - } - - // Guard: Error out if this is not a chart. - if false { - if _, err := os.Stat(filepath.Join(chartPath, "Chart.yaml")); err != nil { - return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart") - } - } - - return lint.AllWithKubeVersion(chartPath, vals, namespace, kubeVersion, helmopts.HelmOptions{}), nil -} diff --git a/pkg/helm/pkg/action/lint_test.go b/pkg/helm/pkg/action/lint_test.go deleted file mode 100644 index 80bf4ce7..00000000 --- a/pkg/helm/pkg/action/lint_test.go +++ /dev/null @@ -1,159 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "testing" -) - -var ( - values = make(map[string]interface{}) - namespace = "testNamespace" - chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1" - chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2" - corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz" - chartWithNoTemplatesDir = "testdata/charts/chart-with-no-templates-dir" -) - -func TestLintChart(t *testing.T) { - tests := []struct { - name string - chartPath string - err bool - }{ - { - name: "decompressed-chart", - chartPath: "testdata/charts/decompressedchart/", - }, - { - name: "archived-chart-path", - chartPath: "testdata/charts/compressedchart-0.1.0.tgz", - }, - { - name: "archived-chart-path-with-hyphens", - chartPath: "testdata/charts/compressedchart-with-hyphens-0.1.0.tgz", - }, - { - name: "archived-tar-gz-chart-path", - chartPath: "testdata/charts/compressedchart-0.1.0.tar.gz", - }, - { - name: "invalid-archived-chart-path", - chartPath: "testdata/charts/invalidcompressedchart0.1.0.tgz", - err: true, - }, - { - name: "chart-missing-manifest", - chartPath: "testdata/charts/chart-missing-manifest", - err: true, - }, - { - name: "chart-with-schema", - chartPath: "testdata/charts/chart-with-schema", - }, - { - name: "chart-with-schema-negative", - chartPath: "testdata/charts/chart-with-schema-negative", - }, - { - name: "pre-release-chart", - chartPath: "testdata/charts/pre-release-chart-0.1.0-alpha.tgz", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, nil) - switch { - case err != nil && !tt.err: - t.Errorf("%s", err) - case err == nil && tt.err: - t.Errorf("Expected a chart parsing error") - } - }) - } -} - -func TestNonExistentChart(t *testing.T) { - t.Run("should error out for non existent tgz chart", func(t *testing.T) { - testCharts := []string{"non-existent-chart.tgz"} - expectedError := "unable to open tarball: open non-existent-chart.tgz: no such file or directory" - testLint := NewLint() - - result := testLint.Run(testCharts, values) - if len(result.Errors) != 1 { - t.Error("expected one error, but got", len(result.Errors)) - } - - actual := result.Errors[0].Error() - if actual != expectedError { - t.Errorf("expected '%s', but got '%s'", expectedError, actual) - } - }) - - t.Run("should error out for corrupted tgz chart", func(t *testing.T) { - testCharts := []string{corruptedTgzChart} - expectedEOFError := "unable to extract tarball: EOF" - testLint := NewLint() - - result := testLint.Run(testCharts, values) - if len(result.Errors) != 1 { - t.Error("expected one error, but got", len(result.Errors)) - } - - actual := result.Errors[0].Error() - if actual != expectedEOFError { - t.Errorf("expected '%s', but got '%s'", expectedEOFError, actual) - } - }) -} - -func TestLint_MultipleCharts(t *testing.T) { - testCharts := []string{chart2MultipleChartLint, chart1MultipleChartLint} - testLint := NewLint() - if result := testLint.Run(testCharts, values); len(result.Errors) > 0 { - t.Error(result.Errors) - } -} - -func TestLint_EmptyResultErrors(t *testing.T) { - testCharts := []string{chart2MultipleChartLint} - testLint := NewLint() - if result := testLint.Run(testCharts, values); len(result.Errors) > 0 { - t.Error("Expected no error, got more") - } -} - -func TestLint_ChartWithWarnings(t *testing.T) { - t.Run("should pass when not strict", func(t *testing.T) { - testCharts := []string{chartWithNoTemplatesDir} - testLint := NewLint() - testLint.Strict = false - if result := testLint.Run(testCharts, values); len(result.Errors) > 0 { - t.Error("Expected no error, got more") - } - }) - - t.Run("should pass with no errors when strict", func(t *testing.T) { - testCharts := []string{chartWithNoTemplatesDir} - testLint := NewLint() - testLint.Strict = true - if result := testLint.Run(testCharts, values); len(result.Errors) != 0 { - t.Error("expected no errors, but got", len(result.Errors)) - } - }) -} diff --git a/pkg/helm/pkg/action/list.go b/pkg/helm/pkg/action/list.go index 994e775d..c63a966b 100644 --- a/pkg/helm/pkg/action/list.go +++ b/pkg/helm/pkg/action/list.go @@ -22,8 +22,9 @@ import ( "k8s.io/apimachinery/pkg/labels" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" + ri "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + releaseutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" ) // ListStates represents zero or more status codes that a list item may have set @@ -139,13 +140,13 @@ type List struct { // NewList constructs a new *List func NewList(cfg *Configuration) *List { return &List{ - StateMask: ListDeployed | ListFailed, + StateMask: ListAll, cfg: cfg, } } // Run executes the list command, returning a set of matches. -func (l *List) Run() ([]*release.Release, error) { +func (l *List) Run() ([]ri.Releaser, error) { if err := l.cfg.KubeClient.IsReachable(); err != nil { return nil, err } @@ -159,9 +160,13 @@ func (l *List) Run() ([]*release.Release, error) { } } - results, err := l.cfg.Releases.List(func(rel *release.Release) bool { + results, err := l.cfg.Releases.List(func(rel ri.Releaser) bool { + r, err := releaserToV1Release(rel) + if err != nil { + return false + } // Skip anything that doesn't match the filter. - if filter != nil && !filter.MatchString(rel.Name) { + if filter != nil && !filter.MatchString(r.Name) { return false } @@ -176,30 +181,35 @@ func (l *List) Run() ([]*release.Release, error) { return results, nil } + rresults, err := releaseListToV1List(results) + if err != nil { + return nil, err + } + // by definition, superseded releases are never shown if // only the latest releases are returned. so if requested statemask // is _only_ ListSuperseded, skip the latest release filter if l.StateMask != ListSuperseded { - results = filterLatestReleases(results) + rresults = filterLatestReleases(rresults) } // State mask application must occur after filtering to // latest releases, otherwise outdated entries can be returned - results = l.filterStateMask(results) + rresults = l.filterStateMask(rresults) // Skip anything that doesn't match the selector selectorObj, err := labels.Parse(l.Selector) if err != nil { return nil, err } - results = l.filterSelector(results, selectorObj) + rresults = l.filterSelector(rresults, selectorObj) // Unfortunately, we have to sort before truncating, which can incur substantial overhead - l.sort(results) + l.sort(rresults) // Guard on offset - if l.Offset >= len(results) { - return []*release.Release{}, nil + if l.Offset >= len(rresults) { + return releaseV1ListToReleaserList([]*release.Release{}) } // Calculate the limit and offset, and then truncate results if necessary. @@ -208,12 +218,12 @@ func (l *List) Run() ([]*release.Release, error) { limit = l.Limit } last := l.Offset + limit - if l := len(results); l < last { + if l := len(rresults); l < last { last = l } - results = results[l.Offset:last] + rresults = rresults[l.Offset:last] - return results, err + return releaseV1ListToReleaserList(rresults) } // sort is an in-place sort where order is based on the value of a.Sort @@ -317,7 +327,7 @@ func (l *List) SetStateMask() { // Apply a default if state == 0 { - state = ListDeployed | ListFailed + state = ListAll } l.StateMask = state diff --git a/pkg/helm/pkg/action/list_test.go b/pkg/helm/pkg/action/list_test.go index 193470e5..c5d8baca 100644 --- a/pkg/helm/pkg/action/list_test.go +++ b/pkg/helm/pkg/action/list_test.go @@ -17,11 +17,16 @@ limitations under the License. package action import ( + "errors" + "io" "testing" "github.com/stretchr/testify/assert" - "github.com/werf/nelm/pkg/helm/pkg/release" + kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" + ri "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/helm/pkg/storage" ) @@ -64,13 +69,14 @@ func TestList_Empty(t *testing.T) { } func newListFixture(t *testing.T) *List { + t.Helper() return NewList(actionConfigFixture(t)) } func TestList_OneNamespace(t *testing.T) { is := assert.New(t) lister := newListFixture(t) - makeMeSomeReleases(lister.cfg.Releases, t) + makeMeSomeReleases(t, lister.cfg.Releases) list, err := lister.Run() is.NoError(err) is.Len(list, 3) @@ -79,7 +85,7 @@ func TestList_OneNamespace(t *testing.T) { func TestList_AllNamespaces(t *testing.T) { is := assert.New(t) lister := newListFixture(t) - makeMeSomeReleases(lister.cfg.Releases, t) + makeMeSomeReleases(t, lister.cfg.Releases) lister.AllNamespaces = true lister.SetStateMask() list, err := lister.Run() @@ -91,9 +97,12 @@ func TestList_Sort(t *testing.T) { is := assert.New(t) lister := newListFixture(t) lister.Sort = ByNameDesc // Other sorts are tested elsewhere - makeMeSomeReleases(lister.cfg.Releases, t) - list, err := lister.Run() + makeMeSomeReleases(t, lister.cfg.Releases) + l, err := lister.Run() is.NoError(err) + list, err := releaseListToV1List(l) + is.NoError(err) + is.Len(list, 3) is.Equal("two", list[0].Name) is.Equal("three", list[1].Name) @@ -104,8 +113,10 @@ func TestList_Limit(t *testing.T) { is := assert.New(t) lister := newListFixture(t) lister.Limit = 2 - makeMeSomeReleases(lister.cfg.Releases, t) - list, err := lister.Run() + makeMeSomeReleases(t, lister.cfg.Releases) + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) is.Len(list, 2) // Lex order means one, three, two @@ -117,8 +128,10 @@ func TestList_BigLimit(t *testing.T) { is := assert.New(t) lister := newListFixture(t) lister.Limit = 20 - makeMeSomeReleases(lister.cfg.Releases, t) - list, err := lister.Run() + makeMeSomeReleases(t, lister.cfg.Releases) + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) is.Len(list, 3) @@ -133,8 +146,10 @@ func TestList_LimitOffset(t *testing.T) { lister := newListFixture(t) lister.Limit = 2 lister.Offset = 1 - makeMeSomeReleases(lister.cfg.Releases, t) - list, err := lister.Run() + makeMeSomeReleases(t, lister.cfg.Releases) + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) is.Len(list, 2) @@ -148,7 +163,7 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) { lister := newListFixture(t) lister.Limit = 2 lister.Offset = 3 // Last item is index 2 - makeMeSomeReleases(lister.cfg.Releases, t) + makeMeSomeReleases(t, lister.cfg.Releases) list, err := lister.Run() is.NoError(err) is.Len(list, 0) @@ -163,24 +178,46 @@ func TestList_LimitOffsetOutOfBounds(t *testing.T) { func TestList_StateMask(t *testing.T) { is := assert.New(t) lister := newListFixture(t) - makeMeSomeReleases(lister.cfg.Releases, t) - one, err := lister.cfg.Releases.Get("one", 1) + makeMeSomeReleases(t, lister.cfg.Releases) + oner, err := lister.cfg.Releases.Get("one", 1) is.NoError(err) - one.SetStatus(release.StatusUninstalled, "uninstalled") + + var one release.Release + switch v := oner.(type) { + case release.Release: + one = v + case *release.Release: + one = *v + default: + t.Fatal("unsupported release type") + } + + one.SetStatus(common.StatusUninstalled, "uninstalled") err = lister.cfg.Releases.Update(one) is.NoError(err) res, err := lister.Run() is.NoError(err) - is.Len(res, 2) - is.Equal("three", res[0].Name) - is.Equal("two", res[1].Name) + is.Len(res, 3) + + ac0, err := ri.NewAccessor(res[0]) + is.NoError(err) + ac1, err := ri.NewAccessor(res[1]) + is.NoError(err) + ac2, err := ri.NewAccessor(res[2]) + is.NoError(err) + + is.Equal("one", ac0.Name()) + is.Equal("three", ac1.Name()) + is.Equal("two", ac2.Name()) lister.StateMask = ListUninstalled res, err = lister.Run() is.NoError(err) is.Len(res, 1) - is.Equal("one", res[0].Name) + ac0, err = ri.NewAccessor(res[0]) + is.NoError(err) + is.Equal("one", ac0.Name()) lister.StateMask |= ListDeployed res, err = lister.Run() @@ -193,7 +230,7 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) { lister := newListFixture(t) lister.StateMask = ListFailed - makeMeSomeReleasesWithStaleFailure(lister.cfg.Releases, t) + makeMeSomeReleasesWithStaleFailure(t, lister.cfg.Releases) res, err := lister.Run() @@ -202,28 +239,30 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) { // "dirty" release should _not_ be present as most recent // release is deployed despite failed release in past - is.Equal("failed", res[0].Name) + ac0, err := ri.NewAccessor(res[0]) + is.NoError(err) + is.Equal("failed", ac0.Name()) } -func makeMeSomeReleasesWithStaleFailure(store *storage.Storage, t *testing.T) { +func makeMeSomeReleasesWithStaleFailure(t *testing.T, store *storage.Storage) { t.Helper() - one := namedReleaseStub("clean", release.StatusDeployed) + one := namedReleaseStub("clean", common.StatusDeployed) one.Namespace = "default" one.Version = 1 - two := namedReleaseStub("dirty", release.StatusDeployed) + two := namedReleaseStub("dirty", common.StatusDeployed) two.Namespace = "default" two.Version = 1 - three := namedReleaseStub("dirty", release.StatusFailed) + three := namedReleaseStub("dirty", common.StatusFailed) three.Namespace = "default" three.Version = 2 - four := namedReleaseStub("dirty", release.StatusDeployed) + four := namedReleaseStub("dirty", common.StatusDeployed) four.Namespace = "default" four.Version = 3 - five := namedReleaseStub("failed", release.StatusFailed) + five := namedReleaseStub("failed", common.StatusFailed) five.Namespace = "default" five.Version = 1 @@ -242,25 +281,27 @@ func TestList_Filter(t *testing.T) { is := assert.New(t) lister := newListFixture(t) lister.Filter = "th." - makeMeSomeReleases(lister.cfg.Releases, t) + makeMeSomeReleases(t, lister.cfg.Releases) res, err := lister.Run() is.NoError(err) is.Len(res, 1) - is.Equal("three", res[0].Name) + ac0, err := ri.NewAccessor(res[0]) + is.NoError(err) + is.Equal("three", ac0.Name()) } func TestList_FilterFailsCompile(t *testing.T) { is := assert.New(t) lister := newListFixture(t) lister.Filter = "t[h.{{{" - makeMeSomeReleases(lister.cfg.Releases, t) + makeMeSomeReleases(t, lister.cfg.Releases) _, err := lister.Run() is.Error(err) } -func makeMeSomeReleases(store *storage.Storage, t *testing.T) { +func makeMeSomeReleases(t *testing.T, store *storage.Storage) { t.Helper() one := releaseStub() one.Name = "one" @@ -366,3 +407,16 @@ func TestSelectorList(t *testing.T) { assert.ElementsMatch(t, expectedFilteredList, res) }) } + +func TestListRun_UnreachableKubeClient(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + lister := NewList(config) + result, err := lister.Run() + + assert.Nil(t, result) + assert.ErrorContains(t, err, "connection refused") +} diff --git a/pkg/helm/pkg/action/package.go b/pkg/helm/pkg/action/package.go index 6b653e17..b6d7e1a0 100644 --- a/pkg/helm/pkg/action/package.go +++ b/pkg/helm/pkg/action/package.go @@ -19,19 +19,23 @@ package action import ( "bufio" "context" + "errors" "fmt" "os" + "path/filepath" "syscall" "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" "golang.org/x/term" + "sigs.k8s.io/yaml" + "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/featgate" + ci "github.com/werf/nelm/pkg/helm/pkg/chart" "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/provenance" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" "github.com/werf/nelm/pkg/ts" ) @@ -43,30 +47,58 @@ type Package struct { Key string Keyring string PassphraseFile string + cachedPassphrase []byte Version string AppVersion string Destination string DependencyUpdate bool - RepositoryConfig string - RepositoryCache string + RepositoryConfig string + RepositoryCache string + PlainHTTP bool + Username string + Password string + CertFile string + KeyFile string + CaFile string + InsecureSkipTLSVerify bool + + TypeScriptOps common.TypeScriptOptions } +const ( + passPhraseFileStdin = "-" +) + // NewPackage creates a new Package object with the given configuration. func NewPackage() *Package { return &Package{} } // Run executes 'helm package' against the given chart and returns the path to the packaged chart. -func (p *Package) Run(path string, _ map[string]interface{}, opts helmopts.HelmOptions) (string, error) { - ch, err := loader.LoadDir(path, opts) +func (p *Package) Run(path string, _ map[string]interface{}) (string, error) { + chrt, err := loader.LoadDir(context.Background(), path) + if err != nil { + return "", err + } + var ch *chart.Chart + switch c := chrt.(type) { + case *chart.Chart: + ch = c + case chart.Chart: + ch = &c + default: + return "", errors.New("invalid chart apiVersion") + } + + ac, err := ci.NewAccessor(ch) if err != nil { return "", err } if featgate.FeatGateTypescript.Enabled() { - if err := ts.BundleChartsRecursive(context.Background(), ch, path, true, opts.TypeScriptOpts.DenoBinaryPath); err != nil { - return "", errors.Wrap(err, "unable to process TypeScript files in chart") + if err := ts.BundleChartsRecursive(context.Background(), ch, path, true, p.TypeScriptOps.DenoBinaryPath); err != nil { + return "", fmt.Errorf("unable to process TypeScript files in chart: %w", err) } } @@ -83,7 +115,7 @@ func (p *Package) Run(path string, _ map[string]interface{}, opts helmopts.HelmO ch.Metadata.AppVersion = p.AppVersion } - if reqs := ch.Metadata.Dependencies; reqs != nil { + if reqs := ac.MetaDependencies(); len(reqs) > 0 { if err := CheckDependencies(ch, reqs); err != nil { return "", err } @@ -103,11 +135,11 @@ func (p *Package) Run(path string, _ map[string]interface{}, opts helmopts.HelmO name, err := chartutil.Save(ch, dest) if err != nil { - return "", errors.Wrap(err, "failed to save") + return "", fmt.Errorf("failed to save: %w", err) } if p.Sign { - err = p.Clearsign(name, opts) + err = p.Clearsign(name) } return name, err @@ -122,7 +154,7 @@ func validateVersion(ver string) error { } // Clearsign signs a chart -func (p *Package) Clearsign(filename string, opts helmopts.HelmOptions) error { +func (p *Package) Clearsign(filename string) error { // Load keyring signer, err := provenance.NewFromKeyring(p.Keyring, p.Key) if err != nil { @@ -131,7 +163,7 @@ func (p *Package) Clearsign(filename string, opts helmopts.HelmOptions) error { passphraseFetcher := promptUser if p.PassphraseFile != "" { - passphraseFetcher, err = passphraseFileFetcher(p.PassphraseFile, os.Stdin) + passphraseFetcher, err = p.passphraseFileFetcher(p.PassphraseFile, os.Stdin) if err != nil { return err } @@ -141,7 +173,35 @@ func (p *Package) Clearsign(filename string, opts helmopts.HelmOptions) error { return err } - sig, err := signer.ClearSign(filename, opts) + // Load the chart archive to extract metadata + chrt, err := loader.LoadFile(context.Background(), filename) + if err != nil { + return fmt.Errorf("failed to load chart for signing: %w", err) + } + var ch *chart.Chart + switch c := chrt.(type) { + case *chart.Chart: + ch = c + case chart.Chart: + ch = &c + default: + return errors.New("invalid chart apiVersion") + } + + // Marshal chart metadata to YAML bytes + metadataBytes, err := yaml.Marshal(ch.Metadata) + if err != nil { + return fmt.Errorf("failed to marshal chart metadata: %w", err) + } + + // Read the chart archive file + archiveData, err := os.ReadFile(filename) + if err != nil { + return fmt.Errorf("failed to read chart archive: %w", err) + } + + // Use the generic provenance signing function + sig, err := signer.ClearSign(archiveData, filepath.Base(filename), metadataBytes) if err != nil { return err } @@ -159,25 +219,42 @@ func promptUser(name string) ([]byte, error) { return pw, err } -func passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) { - file, err := openPassphraseFile(passphraseFile, stdin) - if err != nil { - return nil, err - } - defer file.Close() +func (p *Package) passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) { + // When reading from stdin we cache the passphrase here. If we are + // packaging multiple charts, we reuse the cached passphrase. This + // allows giving the passphrase once on stdin without failing with + // complaints about stdin already being closed. + // + // An alternative to this would be to omit file.Close() for stdin + // below and require the user to provide the same passphrase once + // per chart on stdin, but that does not seem very user-friendly. - reader := bufio.NewReader(file) - passphrase, _, err := reader.ReadLine() - if err != nil { - return nil, err + if p.cachedPassphrase == nil { + file, err := openPassphraseFile(passphraseFile, stdin) + if err != nil { + return nil, err + } + defer file.Close() + + reader := bufio.NewReader(file) + passphrase, _, err := reader.ReadLine() + if err != nil { + return nil, err + } + p.cachedPassphrase = passphrase + + return func(_ string) ([]byte, error) { + return passphrase, nil + }, nil } - return func(name string) ([]byte, error) { - return passphrase, nil + + return func(_ string) ([]byte, error) { + return p.cachedPassphrase, nil }, nil } func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) { - if passphraseFile == "-" { + if passphraseFile == passPhraseFileStdin { stat, err := stdin.Stat() if err != nil { return nil, err diff --git a/pkg/helm/pkg/action/package_test.go b/pkg/helm/pkg/action/package_test.go index c6a253e6..9b6d7705 100644 --- a/pkg/helm/pkg/action/package_test.go +++ b/pkg/helm/pkg/action/package_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/Masterminds/semver/v3" + "github.com/stretchr/testify/require" "github.com/werf/nelm/pkg/helm/intern/test/ensure" ) @@ -29,8 +30,9 @@ import ( func TestPassphraseFileFetcher(t *testing.T) { secret := "secret" directory := ensure.TempFile(t, "passphrase-file", []byte(secret)) + testPkg := NewPackage() - fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil) + fetcher, err := testPkg.passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil) if err != nil { t.Fatal("Unable to create passphraseFileFetcher", err) } @@ -48,8 +50,9 @@ func TestPassphraseFileFetcher(t *testing.T) { func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) { secret := "secret" directory := ensure.TempFile(t, "passphrase-file", []byte(secret+"\n\n.")) + testPkg := NewPackage() - fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil) + fetcher, err := testPkg.passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil) if err != nil { t.Fatal("Unable to create passphraseFileFetcher", err) } @@ -66,17 +69,49 @@ func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) { func TestPassphraseFileFetcher_WithInvalidStdin(t *testing.T) { directory := t.TempDir() + testPkg := NewPackage() stdin, err := os.CreateTemp(directory, "non-existing") if err != nil { t.Fatal("Unable to create test file", err) } - if _, err := passphraseFileFetcher("-", stdin); err == nil { + if _, err := testPkg.passphraseFileFetcher("-", stdin); err == nil { t.Error("Expected passphraseFileFetcher returning an error") } } +func TestPassphraseFileFetcher_WithStdinAndMultipleFetches(t *testing.T) { + testPkg := NewPackage() + stdin, w, err := os.Pipe() + if err != nil { + t.Fatal("Unable to create pipe", err) + } + + passphrase := "secret-from-stdin" + + go func() { + _, err = w.Write([]byte(passphrase + "\n")) + require.NoError(t, err) + }() + + for range 4 { + fetcher, err := testPkg.passphraseFileFetcher("-", stdin) + if err != nil { + t.Errorf("Expected passphraseFileFetcher to not return an error, but got %v", err) + } + + pass, err := fetcher("key") + if err != nil { + t.Errorf("Expected passphraseFileFetcher invocation to succeed, failed with %v", err) + } + + if string(pass) != string(passphrase) { + t.Errorf("Expected multiple passphrase fetch to return %q, got %q", passphrase, pass) + } + } +} + func TestValidateVersion(t *testing.T) { type args struct { ver string @@ -119,3 +154,18 @@ func TestValidateVersion(t *testing.T) { }) } } + +func TestRun_ErrorPath(t *testing.T) { + client := NewPackage() + _, err := client.Run("err-path", nil) + require.Error(t, err) +} + +func TestRun(t *testing.T) { + chartPath := "testdata/charts/chart-with-schema" + client := NewPackage() + filename, err := client.Run(chartPath, nil) + require.NoError(t, err) + require.Equal(t, "empty-0.1.0.tgz", filename) + require.NoError(t, os.Remove(filename)) +} diff --git a/pkg/helm/pkg/action/pull.go b/pkg/helm/pkg/action/pull.go index 35a9b5cc..5226853b 100644 --- a/pkg/helm/pkg/action/pull.go +++ b/pkg/helm/pkg/action/pull.go @@ -22,14 +22,12 @@ import ( "path/filepath" "strings" - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/cli" "github.com/werf/nelm/pkg/helm/pkg/downloader" "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/repo" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) // Pull is the action for checking a given release's information. @@ -56,13 +54,8 @@ func WithConfig(cfg *Configuration) PullOpt { } } -// NewPull creates a new Pull object. -func NewPull() *Pull { - return NewPullWithOpts() -} - -// NewPullWithOpts creates a new pull, with configuration options. -func NewPullWithOpts(opts ...PullOpt) *Pull { +// NewPull creates a new Pull with configuration options. +func NewPull(opts ...PullOpt) *Pull { p := &Pull{} for _, fn := range opts { fn(p) @@ -89,12 +82,13 @@ func (p *Pull) Run(chartRef string) (string, error) { getter.WithBasicAuth(p.Username, p.Password), getter.WithPassCredentialsAll(p.PassCredentialsAll), getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), - getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSverify), + getter.WithInsecureSkipVerifyTLS(p.InsecureSkipTLSVerify), getter.WithPlainHTTP(p.PlainHTTP), }, RegistryClient: p.cfg.RegistryClient, RepositoryConfig: p.Settings.RepositoryConfig, RepositoryCache: p.Settings.RepositoryCache, + ContentCache: p.Settings.ContentCache, } if registry.IsOCI(chartRef) { @@ -116,20 +110,30 @@ func (p *Pull) Run(chartRef string) (string, error) { var err error dest, err = os.MkdirTemp("", "helm-") if err != nil { - return out.String(), errors.Wrap(err, "failed to untar") + return out.String(), fmt.Errorf("failed to untar: %w", err) } defer os.RemoveAll(dest) } + downloadSourceRef := chartRef if p.RepoURL != "" { - chartURL, err := repo.FindChartInAuthAndTLSAndPassRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, p.InsecureSkipTLSverify, p.PassCredentialsAll, getter.All(p.Settings)) + chartURL, err := repo.FindChartInRepoURL( + p.RepoURL, + chartRef, + getter.All(p.Settings), + repo.WithChartVersion(p.Version), + repo.WithClientTLS(p.CertFile, p.KeyFile, p.CaFile), + repo.WithUsernamePassword(p.Username, p.Password), + repo.WithInsecureSkipTLSVerify(p.InsecureSkipTLSVerify), + repo.WithPassCredentialsAll(p.PassCredentialsAll), + ) if err != nil { return out.String(), err } - chartRef = chartURL + downloadSourceRef = chartURL } - saved, v, err := c.DownloadTo(chartRef, p.Version, dest) + saved, v, err := c.DownloadTo(downloadSourceRef, p.Version, dest) if err != nil { return out.String(), err } @@ -159,11 +163,10 @@ func (p *Pull) Run(chartRef string) (string, error) { if _, err := os.Stat(udCheck); err != nil { if err := os.MkdirAll(udCheck, 0755); err != nil { - return out.String(), errors.Wrap(err, "failed to untar (mkdir)") + return out.String(), fmt.Errorf("failed to untar (mkdir): %w", err) } - } else { - return out.String(), errors.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck) + return out.String(), fmt.Errorf("failed to untar: a file or directory with the name %s already exists", udCheck) } return out.String(), chartutil.ExpandFile(ud, saved) diff --git a/pkg/helm/pkg/action/pull_test.go b/pkg/helm/pkg/action/pull_test.go new file mode 100644 index 00000000..e6480694 --- /dev/null +++ b/pkg/helm/pkg/action/pull_test.go @@ -0,0 +1,80 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/werf/nelm/pkg/helm/pkg/cli" + "github.com/werf/nelm/pkg/helm/pkg/registry" +) + +func TestNewPull(t *testing.T) { + config := actionConfigFixture(t) + client := NewPull(WithConfig(config)) + + assert.NotNil(t, client) + assert.Equal(t, config, client.cfg) +} + +func TestPullSetRegistryClient(t *testing.T) { + config := actionConfigFixture(t) + client := NewPull(WithConfig(config)) + + registryClient := ®istry.Client{} + client.SetRegistryClient(registryClient) + assert.Equal(t, registryClient, client.cfg.RegistryClient) +} + +func TestPullRun_ChartNotFound(t *testing.T) { + srv, err := startLocalServerForTests(t, nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + config := actionConfigFixture(t) + client := NewPull(WithConfig(config)) + client.Settings = cli.New() + client.RepoURL = srv.URL + + chartRef := "nginx" + _, err = client.Run(chartRef) + require.ErrorContains(t, err, "404 Not Found") +} + +func startLocalServerForTests(t *testing.T, handler http.Handler) (*httptest.Server, error) { + t.Helper() + if handler == nil { + fileBytes, err := os.ReadFile("../repo/v1/testdata/local-index.yaml") + if err != nil { + return nil, err + } + handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, err = w.Write(fileBytes) + require.NoError(t, err) + }) + } + + return httptest.NewServer(handler), nil +} diff --git a/pkg/helm/pkg/action/push.go b/pkg/helm/pkg/action/push.go index 1ca56b1b..1efe5edc 100644 --- a/pkg/helm/pkg/action/push.go +++ b/pkg/helm/pkg/action/push.go @@ -24,7 +24,6 @@ import ( "github.com/werf/nelm/pkg/helm/pkg/pusher" "github.com/werf/nelm/pkg/helm/pkg/registry" "github.com/werf/nelm/pkg/helm/pkg/uploader" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // Push is the action for uploading a chart. @@ -36,7 +35,7 @@ type Push struct { certFile string keyFile string caFile string - insecureSkipTLSverify bool + insecureSkipTLSVerify bool plainHTTP bool out io.Writer } @@ -63,7 +62,7 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) PushOpt { // WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) PushOpt { return func(p *Push) { - p.insecureSkipTLSverify = insecureSkipTLSVerify + p.insecureSkipTLSVerify = insecureSkipTLSVerify } } @@ -74,7 +73,7 @@ func WithPlainHTTP(plainHTTP bool) PushOpt { } } -// WithOptWriter sets the registryOut field on the push configuration object. +// WithPushOptWriter sets the registryOut field on the push configuration object. func WithPushOptWriter(out io.Writer) PushOpt { return func(p *Push) { p.out = out @@ -91,7 +90,7 @@ func NewPushWithOpts(opts ...PushOpt) *Push { } // Run executes 'helm push' against the given chart archive. -func (p *Push) Run(chartRef string, remote string, opts helmopts.HelmOptions) (string, error) { +func (p *Push) Run(chartRef string, remote string) (string, error) { var out strings.Builder c := uploader.ChartUploader{ @@ -99,7 +98,7 @@ func (p *Push) Run(chartRef string, remote string, opts helmopts.HelmOptions) (s Pushers: pusher.All(p.Settings), Options: []pusher.Option{ pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile), - pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSverify), + pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify), pusher.WithPlainHTTP(p.plainHTTP), }, } @@ -109,5 +108,5 @@ func (p *Push) Run(chartRef string, remote string, opts helmopts.HelmOptions) (s c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient)) } - return out.String(), c.UploadTo(chartRef, remote, opts) + return out.String(), c.UploadTo(chartRef, remote) } diff --git a/pkg/helm/pkg/action/push_test.go b/pkg/helm/pkg/action/push_test.go new file mode 100644 index 00000000..35c6f3ef --- /dev/null +++ b/pkg/helm/pkg/action/push_test.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewPushWithPushConfig(t *testing.T) { + config := actionConfigFixture(t) + client := NewPushWithOpts(WithPushConfig(config)) + + assert.NotNil(t, client) + assert.Equal(t, config, client.cfg) +} + +func TestNewPushWithTLSClientConfig(t *testing.T) { + certFile := "certFile" + keyFile := "keyFile" + caFile := "caFile" + client := NewPushWithOpts(WithTLSClientConfig(certFile, keyFile, caFile)) + + assert.NotNil(t, client) + assert.Equal(t, certFile, client.certFile) + assert.Equal(t, keyFile, client.keyFile) + assert.Equal(t, caFile, client.caFile) +} + +func TestNewPushWithInsecureSkipTLSVerify(t *testing.T) { + client := NewPushWithOpts(WithInsecureSkipTLSVerify(true)) + + assert.NotNil(t, client) + assert.Equal(t, true, client.insecureSkipTLSVerify) +} + +func TestNewPushWithPlainHTTP(t *testing.T) { + client := NewPushWithOpts(WithPlainHTTP(true)) + + assert.NotNil(t, client) + assert.Equal(t, true, client.plainHTTP) +} + +func TestNewPushWithPushOptWriter(t *testing.T) { + buf := new(bytes.Buffer) + client := NewPushWithOpts(WithPushOptWriter(buf)) + + assert.NotNil(t, client) + assert.Equal(t, buf, client.out) +} diff --git a/pkg/helm/pkg/action/registry_login.go b/pkg/helm/pkg/action/registry_login.go index cc60eb7a..ef2535f2 100644 --- a/pkg/helm/pkg/action/registry_login.go +++ b/pkg/helm/pkg/action/registry_login.go @@ -24,11 +24,12 @@ import ( // RegistryLogin performs a registry login operation. type RegistryLogin struct { - cfg *Configuration - certFile string - keyFile string - caFile string - insecure bool + cfg *Configuration + certFile string + keyFile string + caFile string + insecure bool + plainHTTP bool } type RegistryLoginOpt func(*RegistryLogin) error @@ -41,7 +42,7 @@ func WithCertFile(certFile string) RegistryLoginOpt { } } -// WithKeyFile specifies whether to very certificates when communicating. +// WithInsecure specifies whether to verify certificates. func WithInsecure(insecure bool) RegistryLoginOpt { return func(r *RegistryLogin) error { r.insecure = insecure @@ -65,6 +66,14 @@ func WithCAFile(caFile string) RegistryLoginOpt { } } +// WithPlainHTTPLogin use http rather than https for login. +func WithPlainHTTPLogin(isPlain bool) RegistryLoginOpt { + return func(r *RegistryLogin) error { + r.plainHTTP = isPlain + return nil + } +} + // NewRegistryLogin creates a new RegistryLogin object with the given configuration. func NewRegistryLogin(cfg *Configuration) *RegistryLogin { return &RegistryLogin{ @@ -84,5 +93,7 @@ func (a *RegistryLogin) Run(_ io.Writer, hostname string, username string, passw hostname, registry.LoginOptBasicAuth(username, password), registry.LoginOptInsecure(a.insecure), - registry.LoginOptTLSClientConfig(a.certFile, a.keyFile, a.caFile)) + registry.LoginOptTLSClientConfig(a.certFile, a.keyFile, a.caFile), + registry.LoginOptPlainText(a.plainHTTP), + ) } diff --git a/pkg/helm/pkg/action/registry_login_test.go b/pkg/helm/pkg/action/registry_login_test.go new file mode 100644 index 00000000..de2450d9 --- /dev/null +++ b/pkg/helm/pkg/action/registry_login_test.go @@ -0,0 +1,84 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewRegistryLogin(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogin(config) + + assert.NotNil(t, client) + assert.Equal(t, config, client.cfg) +} + +func TestWithCertFile(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogin(config) + + certFile := "testdata/cert.pem" + opt := WithCertFile(certFile) + + assert.Nil(t, opt(client)) + assert.Equal(t, certFile, client.certFile) +} + +func TestWithInsecure(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogin(config) + + opt := WithInsecure(true) + + assert.Nil(t, opt(client)) + assert.Equal(t, true, client.insecure) +} + +func TestWithKeyFile(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogin(config) + + keyFile := "testdata/key.pem" + opt := WithKeyFile(keyFile) + + assert.Nil(t, opt(client)) + assert.Equal(t, keyFile, client.keyFile) +} + +func TestWithCAFile(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogin(config) + + caFile := "testdata/ca.pem" + opt := WithCAFile(caFile) + + assert.Nil(t, opt(client)) + assert.Equal(t, caFile, client.caFile) +} + +func TestWithPlainHTTPLogin(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogin(config) + + opt := WithPlainHTTPLogin(true) + + assert.Nil(t, opt(client)) + assert.Equal(t, true, client.plainHTTP) +} diff --git a/pkg/helm/pkg/action/registry_logout_test.go b/pkg/helm/pkg/action/registry_logout_test.go new file mode 100644 index 00000000..669d9c9b --- /dev/null +++ b/pkg/helm/pkg/action/registry_logout_test.go @@ -0,0 +1,31 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewRegistryLogout(t *testing.T) { + config := actionConfigFixture(t) + client := NewRegistryLogout(config) + + assert.NotNil(t, client) + assert.Equal(t, config, client.cfg) +} diff --git a/pkg/helm/pkg/action/resource_policy.go b/pkg/helm/pkg/action/resource_policy.go deleted file mode 100644 index 268f40a8..00000000 --- a/pkg/helm/pkg/action/resource_policy.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "strings" - - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" -) - -func filterManifestsToKeep(manifests []releaseutil.Manifest) (keep, remaining []releaseutil.Manifest) { - for _, m := range manifests { - if m.Head.Metadata == nil || m.Head.Metadata.Annotations == nil || len(m.Head.Metadata.Annotations) == 0 { - remaining = append(remaining, m) - continue - } - - resourcePolicyType, ok := m.Head.Metadata.Annotations[kube.ResourcePolicyAnno] - if !ok { - remaining = append(remaining, m) - continue - } - - resourcePolicyType = strings.ToLower(strings.TrimSpace(resourcePolicyType)) - if resourcePolicyType == kube.KeepPolicy { - keep = append(keep, m) - } - - } - return keep, remaining -} diff --git a/pkg/helm/pkg/action/show.go b/pkg/helm/pkg/action/show.go new file mode 100644 index 00000000..3a3b6da0 --- /dev/null +++ b/pkg/helm/pkg/action/show.go @@ -0,0 +1,157 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "bytes" + "context" + "fmt" + "strings" + + "k8s.io/cli-runtime/pkg/printers" + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" + "github.com/werf/nelm/pkg/helm/pkg/registry" +) + +// ShowOutputFormat is the format of the output of `helm show` +type ShowOutputFormat string + +const ( + // ShowAll is the format which shows all the information of a chart + ShowAll ShowOutputFormat = "all" + // ShowChart is the format which only shows the chart's definition + ShowChart ShowOutputFormat = "chart" + // ShowValues is the format which only shows the chart's values + ShowValues ShowOutputFormat = "values" + // ShowReadme is the format which only shows the chart's README + ShowReadme ShowOutputFormat = "readme" + // ShowCRDs is the format which only shows the chart's CRDs + ShowCRDs ShowOutputFormat = "crds" +) + +var readmeFileNames = []string{"readme.md", "readme.txt", "readme"} + +func (o ShowOutputFormat) String() string { + return string(o) +} + +// Show is the action for checking a given release's information. +// +// It provides the implementation of 'helm show' and its respective subcommands. +type Show struct { + ChartPathOptions + Devel bool + OutputFormat ShowOutputFormat + JSONPathTemplate string + chart *chart.Chart // for testing +} + +// NewShow creates a new Show object with the given configuration. +func NewShow(output ShowOutputFormat, cfg *Configuration) *Show { + sh := &Show{ + OutputFormat: output, + } + sh.registryClient = cfg.RegistryClient + + return sh +} + +// SetRegistryClient sets the registry client to use when pulling a chart from a registry. +func (s *Show) SetRegistryClient(client *registry.Client) { + s.registryClient = client +} + +// Run executes 'helm show' against the given release. +func (s *Show) Run(chartpath string) (string, error) { + if s.chart == nil { + chrt, err := loader.Load(context.Background(), chartpath) + if err != nil { + return "", err + } + s.chart = chrt + } + cf, err := yaml.Marshal(s.chart.Metadata) + if err != nil { + return "", err + } + + var out strings.Builder + if s.OutputFormat == ShowChart || s.OutputFormat == ShowAll { + fmt.Fprintf(&out, "%s\n", cf) + } + + if (s.OutputFormat == ShowValues || s.OutputFormat == ShowAll) && s.chart.Values != nil { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + if s.JSONPathTemplate != "" { + printer, err := printers.NewJSONPathPrinter(s.JSONPathTemplate) + if err != nil { + return "", fmt.Errorf("error parsing jsonpath %s: %w", s.JSONPathTemplate, err) + } + printer.Execute(&out, s.chart.Values) + } else { + for _, f := range s.chart.Raw { + if f.Name == chartutil.ValuesfileName { + fmt.Fprintln(&out, string(f.Data)) + } + } + } + } + + if s.OutputFormat == ShowReadme || s.OutputFormat == ShowAll { + readme := findReadme(s.chart.Files) + if readme != nil { + if s.OutputFormat == ShowAll { + fmt.Fprintln(&out, "---") + } + fmt.Fprintf(&out, "%s\n", readme.Data) + } + } + + if s.OutputFormat == ShowCRDs || s.OutputFormat == ShowAll { + crds := s.chart.CRDObjects() + if len(crds) > 0 { + for _, crd := range crds { + if !bytes.HasPrefix(crd.File.Data, []byte("---")) { + fmt.Fprintln(&out, "---") + } + fmt.Fprintf(&out, "%s\n", string(crd.File.Data)) + } + } + } + return out.String(), nil +} + +func findReadme(files []*common.File) (file *common.File) { + for _, file := range files { + for _, n := range readmeFileNames { + if file == nil { + continue + } + if strings.EqualFold(file.Name, n) { + return file + } + } + } + return nil +} diff --git a/pkg/helm/pkg/action/show_test.go b/pkg/helm/pkg/action/show_test.go index 9328229d..eadf3ce5 100644 --- a/pkg/helm/pkg/action/show_test.go +++ b/pkg/helm/pkg/action/show_test.go @@ -18,23 +18,30 @@ package action import ( "testing" + "time" - "github.com/werf/nelm/pkg/helm/pkg/chart" + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/registry" ) func TestShow(t *testing.T) { config := actionConfigFixture(t) - client := NewShowWithConfig(ShowAll, config) + client := NewShow(ShowAll, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ - {Name: "README.md", Data: []byte("README\n")}, - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + Files: []*common.File{ + {Name: "README.md", ModTime: modTime, Data: []byte("README\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, + {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")}, }, - Raw: []*chart.File{ - {Name: "values.yaml", Data: []byte("VALUES\n")}, + Raw: []*common.File{ + {Name: "values.yaml", ModTime: modTime, Data: []byte("VALUES\n")}, }, Values: map[string]interface{}{}, } @@ -58,6 +65,9 @@ foo --- bar +--- +baz + ` if output != expect { t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) @@ -65,7 +75,8 @@ bar } func TestShowNoValues(t *testing.T) { - client := NewShow(ShowAll) + config := actionConfigFixture(t) + client := NewShow(ShowAll, config) client.chart = new(chart.Chart) // Regression tests for missing values. See issue #1024. @@ -81,7 +92,8 @@ func TestShowNoValues(t *testing.T) { } func TestShowValuesByJsonPathFormat(t *testing.T) { - client := NewShow(ShowValues) + config := actionConfigFixture(t) + client := NewShow(ShowValues, config) client.JSONPathTemplate = "{$.nestedKey.simpleKey}" client.chart = buildChart(withSampleValues()) output, err := client.Run("") @@ -95,13 +107,16 @@ func TestShowValuesByJsonPathFormat(t *testing.T) { } func TestShowCRDs(t *testing.T) { - client := NewShow(ShowCRDs) + config := actionConfigFixture(t) + client := NewShow(ShowCRDs, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + Files: []*common.File{ + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, + {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")}, }, } @@ -116,6 +131,9 @@ foo --- bar +--- +baz + ` if output != expect { t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) @@ -123,13 +141,15 @@ bar } func TestShowNoReadme(t *testing.T) { - client := NewShow(ShowAll) + config := actionConfigFixture(t) + client := NewShow(ShowAll, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, - Files: []*chart.File{ - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + Files: []*common.File{ + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, }, } @@ -151,3 +171,12 @@ bar t.Errorf("Expected\n%q\nGot\n%q\n", expect, output) } } + +func TestShowSetRegistryClient(t *testing.T) { + config := actionConfigFixture(t) + client := NewShow(ShowAll, config) + + registryClient := ®istry.Client{} + client.SetRegistryClient(registryClient) + assert.Equal(t, registryClient, client.registryClient) +} diff --git a/pkg/helm/pkg/action/status.go b/pkg/helm/pkg/action/status.go new file mode 100644 index 00000000..5bb1312a --- /dev/null +++ b/pkg/helm/pkg/action/status.go @@ -0,0 +1,83 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "bytes" + + "github.com/werf/nelm/pkg/helm/pkg/kube" + ri "github.com/werf/nelm/pkg/helm/pkg/release" +) + +// Status is the action for checking the deployment status of releases. +// +// It provides the implementation of 'helm status'. +type Status struct { + cfg *Configuration + + Version int + + // ShowResourcesTable is used with ShowResources. When true this will cause + // the resulting objects to be retrieved as a kind=table. + ShowResourcesTable bool +} + +// NewStatus creates a new Status object with the given configuration. +func NewStatus(cfg *Configuration) *Status { + return &Status{ + cfg: cfg, + } +} + +// Run executes 'helm status' against the given release. +func (s *Status) Run(name string) (ri.Releaser, error) { + if err := s.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + + reli, err := s.cfg.releaseContent(name, s.Version) + if err != nil { + return nil, err + } + + rel, err := releaserToV1Release(reli) + if err != nil { + return nil, err + } + + var resources kube.ResourceList + if s.ShowResourcesTable { + resources, err = s.cfg.KubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false) + if err != nil { + return nil, err + } + } else { + resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false) + if err != nil { + return nil, err + } + } + + resp, err := s.cfg.KubeClient.Get(resources, true) + if err != nil { + return nil, err + } + + rel.Info.Resources = resp + + return rel, nil +} diff --git a/pkg/helm/pkg/action/status_test.go b/pkg/helm/pkg/action/status_test.go new file mode 100644 index 00000000..b7ef5ac8 --- /dev/null +++ b/pkg/helm/pkg/action/status_test.go @@ -0,0 +1,143 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "errors" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" + rcommon "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +func TestNewStatus(t *testing.T) { + config := actionConfigFixture(t) + client := NewStatus(config) + + assert.NotNil(t, client) + assert.Equal(t, config, client.cfg) + assert.Equal(t, 0, client.Version) +} + +func TestStatusRun(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.BuildDummy = true + config.KubeClient = &failingKubeClient + client := NewStatus(config) + client.ShowResourcesTable = true + + releaseName := "test-release" + require.NoError(t, configureReleaseContent(config, releaseName)) + releaser, err := client.Run(releaseName) + require.NoError(t, err) + + result, err := releaserToV1Release(releaser) + require.NoError(t, err) + assert.Equal(t, releaseName, result.Name) + assert.Equal(t, 1, result.Version) +} + +func TestStatusRun_KubeClientNotReachable(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + client := NewStatus(config) + + result, err := client.Run("") + assert.Nil(t, result) + assert.Error(t, err) +} + +func TestStatusRun_KubeClientBuildTableError(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.BuildTableError = errors.New("build table error") + config.KubeClient = &failingKubeClient + + releaseName := "test-release" + require.NoError(t, configureReleaseContent(config, releaseName)) + + client := NewStatus(config) + client.ShowResourcesTable = true + + result, err := client.Run(releaseName) + + assert.Nil(t, result) + assert.ErrorContains(t, err, "build table error") +} + +func TestStatusRun_KubeClientBuildError(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.BuildError = errors.New("build error") + config.KubeClient = &failingKubeClient + + releaseName := "test-release" + require.NoError(t, configureReleaseContent(config, releaseName)) + + client := NewStatus(config) + client.ShowResourcesTable = false + + result, err := client.Run(releaseName) + assert.Nil(t, result) + assert.ErrorContains(t, err, "build error") +} + +func TestStatusRun_KubeClientGetError(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.BuildError = errors.New("get error") + config.KubeClient = &failingKubeClient + + releaseName := "test-release" + require.NoError(t, configureReleaseContent(config, releaseName)) + client := NewStatus(config) + + result, err := client.Run(releaseName) + assert.Nil(t, result) + assert.ErrorContains(t, err, "get error") +} + +func configureReleaseContent(cfg *Configuration, releaseName string) error { + rel := &release.Release{ + Name: releaseName, + Info: &release.Info{ + Status: rcommon.StatusDeployed, + }, + Manifest: testManifest, + Version: 1, + Namespace: "default", + } + + return cfg.Releases.Create(rel) +} + +const testManifest = ` +apiVersion: v1 +kind: Pod +metadata: + namespace: default + name: test-application +` diff --git a/pkg/helm/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml b/pkg/helm/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml index 3cb66daf..98c70aad 100755 --- a/pkg/helm/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml +++ b/pkg/helm/pkg/action/testdata/charts/chart-with-uncompressed-dependencies/values.yaml @@ -74,7 +74,7 @@ externalDatabase: ## Database host host: localhost - ## non-root Username for Wordpress Database + ## non-root Username for WordPress Database user: bn_wordpress ## Database password @@ -102,7 +102,7 @@ mariadb: db: name: bitnami_wordpress user: bn_wordpress - ## If the password is not specified, mariadb will generates a random password + ## If the password is not specified, mariadb will generate a random password ## # password: @@ -165,7 +165,7 @@ readinessProbe: successThreshold: 1 ## Configure the ingress resource that allows you to access the -## Wordpress installation. Set up the URL +## WordPress installation. Set up the URL ## ref: http://kubernetes.io/docs/user-guide/ingress/ ## ingress: diff --git a/pkg/helm/pkg/action/uninstall.go b/pkg/helm/pkg/action/uninstall.go index be686416..1cf68396 100644 --- a/pkg/helm/pkg/action/uninstall.go +++ b/pkg/helm/pkg/action/uninstall.go @@ -17,313 +17,29 @@ limitations under the License. package action import ( - "context" - "fmt" "strings" - "time" - - "github.com/pkg/errors" - - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases" - "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" - apierrors "k8s.io/apimachinery/pkg/api/errors" ) -// Uninstall is the action for uninstalling releases. -// -// It provides the implementation of 'helm uninstall'. -type Uninstall struct { - cfg *Configuration - - DisableHooks bool - DryRun bool - IgnoreNotFound bool - KeepHistory bool - Wait bool - DeletionPropagation string - Timeout time.Duration - Description string - - DeleteHooks bool - DeleteNamespace bool - Namespace string - StagesSplitter phases.Splitter -} - -// NewUninstall creates a new Uninstall object with the given configuration. -func NewUninstall(cfg *Configuration, stagesSplitter phases.Splitter) *Uninstall { - if stagesSplitter == nil { - stagesSplitter = &phases.SingleStageSplitter{} - } - - return &Uninstall{ - cfg: cfg, - - StagesSplitter: stagesSplitter, - } -} - -// Run uninstalls the given release. -func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) { - if err := u.cfg.KubeClient.IsReachable(); err != nil { - return nil, err - } - - if u.DryRun { - // In the dry run case, just see if the release exists - r, err := u.cfg.releaseContent(name, 0) - if err != nil { - if apierrors.IsNotFound(err) && u.IgnoreNotFound { - u.cfg.Log("No such release %q", name) - return &release.UninstallReleaseResponse{}, nil - } - - return &release.UninstallReleaseResponse{}, err - } - return &release.UninstallReleaseResponse{Release: r}, nil - } - - if err := chartutil.ValidateReleaseName(name); err != nil { - return nil, errors.Errorf("uninstall: Release name is invalid: %s", name) - } - - rels, err := u.cfg.Releases.History(name) - if err != nil { - if u.IgnoreNotFound && errors.Is(err, driver.ErrReleaseNotFound) { - u.cfg.Log("No such release %q", name) - - if u.DeleteNamespace && !u.KeepHistory { - if err := u.cfg.KubeClient.DeleteNamespace(context.Background(), u.Namespace, kube.DeleteOptions{Wait: true, WaitTimeout: u.Timeout}); err != nil { - if kube.IsNotFound(err) { - u.cfg.Log("No such namespace %q", u.Namespace) - return &release.UninstallReleaseResponse{}, nil - } - return nil, errors.Wrapf(err, "unable to delete namespace %s", u.Namespace) - } - } - - return nil, nil - } - return nil, errors.Wrapf(err, "uninstall: Release not loaded: %s", name) - } - if len(rels) < 1 { - return nil, errMissingRelease - } - - releaseutil.SortByRevision(rels) - rel := rels[len(rels)-1] - - // TODO: Are there any cases where we want to force a delete even if it's - // already marked deleted? - if rel.Info.Status == release.StatusUninstalled { - if u.IgnoreNotFound { - return &release.UninstallReleaseResponse{Release: rel}, nil - } - if !u.KeepHistory { - if err := u.purgeReleases(rels...); err != nil { - return nil, errors.Wrap(err, "uninstall: Failed to purge the release") - } - return &release.UninstallReleaseResponse{Release: rel}, nil - } - return nil, errors.Errorf("the release named %q is already deleted", name) - } - - u.cfg.Log("uninstall: Deleting %s", name) - rel.Info.Status = release.StatusUninstalling - rel.Info.Deleted = helmtime.Now() - rel.Info.Description = "Deletion in progress (or silently failed)" - res := &release.UninstallReleaseResponse{Release: rel} - - if !u.DisableHooks { - if err := u.cfg.execHook(rel, release.HookPreDelete, u.Timeout); err != nil { - return res, err - } - } else { - u.cfg.Log("delete hooks disabled for %s", name) - } - - deployedResourcesCalculator := phases.NewDeployedResourcesCalculator(rels, u.StagesSplitter, u.cfg.KubeClient) - deployedResources, err := deployedResourcesCalculator.Calculate() - if err != nil { - return nil, fmt.Errorf("error calculating deployed resources: %w", err) - } - - release.SetUninstallPhaseStageInfo(rel) - - // From here on out, the release is currently considered to be in StatusUninstalling - // state. - if err := u.cfg.Releases.Update(rel); err != nil { - u.cfg.Log("uninstall: Failed to store updated release: %s", err) - } - - deletedResources, kept, errs := u.deleteRelease(rel, deployedResources) - if errs != nil { - u.cfg.Log("uninstall: Failed to delete release: %s", errs) - return nil, errors.Errorf("failed to delete release: %s", name) - } - - if kept != "" { - kept = "These resources were kept due to the resource policy:\n" + kept - } - res.Info = kept - - if u.Wait { - if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceExt); ok { - if err := kubeClient.WaitForDelete(deletedResources, u.Timeout); err != nil { - errs = append(errs, err) - } - } - } - - if !u.DisableHooks { - if err := u.cfg.execHook(rel, release.HookPostDelete, u.Timeout); err != nil { - errs = append(errs, err) - } - } - - if u.DeleteHooks { - var hooksFromAllReleases []*release.Hook - for _, r := range rels { - hooksFromAllReleases = append(hooksFromAllReleases, r.Hooks...) - } - if len(hooksFromAllReleases) > 0 { - if err := u.cfg.deleteHooks(hooksFromAllReleases); err != nil { - errs = append(errs, err) - } - } - } - - rel.Info.Status = release.StatusUninstalled - if len(u.Description) > 0 { - rel.Info.Description = u.Description - } else { - rel.Info.Description = "Uninstallation complete" - } - - if !u.KeepHistory { - u.cfg.Log("purge requested for %s", name) - err := u.purgeReleases(rels...) - if err != nil { - errs = append(errs, errors.Wrap(err, "uninstall: Failed to purge the release")) - } - - // Return the errors that occurred while deleting the release, if any - if len(errs) > 0 { - return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs)) - } - - if u.DeleteNamespace { - if err := u.cfg.KubeClient.DeleteNamespace(context.Background(), u.Namespace, kube.DeleteOptions{Wait: true, WaitTimeout: u.Timeout}); err != nil { - return res, errors.Wrapf(err, "unable to delete namespace %s", u.Namespace) - } - } - - return res, nil - } - - if err := u.cfg.Releases.Update(rel); err != nil { - u.cfg.Log("uninstall: Failed to store updated release: %s", err) - } - - if len(errs) > 0 { - return res, errors.Errorf("uninstallation completed with %d error(s): %s", len(errs), joinErrors(errs)) - } - return res, nil -} - -func (u *Uninstall) purgeReleases(rels ...*release.Release) error { - for _, rel := range rels { - if _, err := u.cfg.Releases.Delete(rel.Name, rel.Version); err != nil { - return err - } - } - return nil +type joinedErrors struct { + errs []error + sep string } -func joinErrors(errs []error) string { - es := make([]string, 0, len(errs)) - for _, e := range errs { - es = append(es, e.Error()) +func joinErrors(errs []error, sep string) error { + return &joinedErrors{ + errs: errs, + sep: sep, } - return strings.Join(es, "; ") } -// deleteRelease deletes the release and returns list of delete resources and manifests that were kept in the deletion process -func (u *Uninstall) deleteRelease(rel *release.Release, res kube.ResourceList) (kube.ResourceList, string, []error) { - manifestsStr, err := res.ToYamlDocs() - if err != nil { - return nil, "", []error{fmt.Errorf("error converting resource list to yaml manifests: %w", err)} - } - - var errs []error - caps, err := u.cfg.getCapabilities() - if err != nil { - return nil, manifestsStr, []error{errors.Wrap(err, "could not get apiVersions from Kubernetes")} - } - - manifests := releaseutil.SplitManifests(manifestsStr) - _, files, err := releaseutil.SortManifests(manifests, caps.APIVersions, releaseutil.UninstallOrder) - if err != nil { - // We could instead just delete everything in no particular order. - // FIXME: One way to delete at this point would be to try a label-based - // deletion. The problem with this is that we could get a false positive - // and delete something that was not legitimately part of this release. - return nil, manifestsStr, []error{errors.Wrap(err, "corrupted release record. You must manually delete the resources")} +func (e *joinedErrors) Error() string { + errs := make([]string, 0, len(e.errs)) + for _, err := range e.errs { + errs = append(errs, err.Error()) } - - filesToKeep, filesToDelete := filterManifestsToKeep(files) - var kept string - for _, f := range filesToKeep { - kept += "[" + f.Head.Kind + "] " + f.Head.Metadata.Name + "\n" - } - - var builder strings.Builder - for _, file := range filesToDelete { - builder.WriteString("\n---\n" + file.Content) - } - - resources, err := u.cfg.KubeClient.Build(strings.NewReader(builder.String()), false) - if err != nil { - return nil, "", []error{errors.Wrap(err, "unable to build kubernetes objects for delete")} - } - if len(resources) > 0 { - if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok { - _, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.cfg, u.DeletionPropagation), kube.DeleteOptions{ - Wait: true, - SkipIfInvalidOwnership: true, - ReleaseName: rel.Name, - ReleaseNamespace: rel.Namespace, - }) - return resources, kept, errs - } - - _, errs = u.cfg.KubeClient.Delete(resources, kube.DeleteOptions{ - Wait: true, - SkipIfInvalidOwnership: true, - ReleaseName: rel.Name, - ReleaseNamespace: rel.Namespace, - }) - } - return resources, kept, errs + return strings.Join(errs, e.sep) } -func parseCascadingFlag(cfg *Configuration, cascadingFlag string) v1.DeletionPropagation { - switch cascadingFlag { - case "orphan": - return v1.DeletePropagationOrphan - case "foreground": - return v1.DeletePropagationForeground - case "background": - return v1.DeletePropagationBackground - default: - cfg.Log("uninstall: given cascade value: %s, defaulting to delete propagation background", cascadingFlag) - return v1.DeletePropagationBackground - } +func (e *joinedErrors) Unwrap() []error { + return e.errs } diff --git a/pkg/helm/pkg/action/uninstall_test.go b/pkg/helm/pkg/action/uninstall_test.go deleted file mode 100644 index 3a22be5f..00000000 --- a/pkg/helm/pkg/action/uninstall_test.go +++ /dev/null @@ -1,140 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - - kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" - "github.com/werf/nelm/pkg/helm/pkg/release" -) - -func uninstallAction(t *testing.T) *Uninstall { - config := actionConfigFixture(t) - unAction := NewUninstall(config, nil) - return unAction -} - -func TestUninstallRelease_ignoreNotFound(t *testing.T) { - unAction := uninstallAction(t) - unAction.DryRun = false - unAction.IgnoreNotFound = true - - is := assert.New(t) - res, err := unAction.Run("release-non-exist") - is.Nil(res) - is.NoError(err) -} - -func TestUninstallRelease_deleteRelease(t *testing.T) { - is := assert.New(t) - - unAction := uninstallAction(t) - unAction.DisableHooks = true - unAction.DryRun = false - unAction.KeepHistory = true - - rel := releaseStub() - rel.Name = "keep-secret" - rel.Manifest = `{ - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "secret", - "annotations": { - "helm.sh/resource-policy": "keep" - } - }, - "type": "Opaque", - "data": { - "password": "password" - } - }` - unAction.cfg.Releases.Create(rel) - res, err := unAction.Run(rel.Name) - is.NoError(err) - expected := `These resources were kept due to the resource policy: -[Secret] secret -` - is.Contains(res.Info, expected) -} - -func TestUninstallRelease_Wait(t *testing.T) { - is := assert.New(t) - - unAction := uninstallAction(t) - unAction.DisableHooks = true - unAction.DryRun = false - unAction.Wait = true - - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Manifest = `{ - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "secret" - }, - "type": "Opaque", - "data": { - "password": "password" - } - }` - unAction.cfg.Releases.Create(rel) - failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("U timed out") - unAction.cfg.KubeClient = failer - res, err := unAction.Run(rel.Name) - is.Error(err) - is.Contains(err.Error(), "U timed out") - is.Equal(res.Release.Info.Status, release.StatusUninstalled) -} - -func TestUninstallRelease_Cascade(t *testing.T) { - is := assert.New(t) - - unAction := uninstallAction(t) - unAction.DisableHooks = true - unAction.DryRun = false - unAction.Wait = false - unAction.DeletionPropagation = "foreground" - - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Manifest = `{ - "apiVersion": "v1", - "kind": "Secret", - "metadata": { - "name": "secret" - }, - "type": "Opaque", - "data": { - "password": "password" - } - }` - unAction.cfg.Releases.Create(rel) - failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.DeleteWithPropagationError = fmt.Errorf("Uninstall with cascade failed") - failer.BuildDummy = true - unAction.cfg.KubeClient = failer - _, err := unAction.Run(rel.Name) - is.Error(err) - is.Contains(err.Error(), "failed to delete release: come-fail-away") -} diff --git a/pkg/helm/pkg/action/upgrade_test.go b/pkg/helm/pkg/action/upgrade_test.go deleted file mode 100644 index 72f78716..00000000 --- a/pkg/helm/pkg/action/upgrade_test.go +++ /dev/null @@ -1,537 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "context" - "fmt" - "reflect" - "testing" - "time" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" - "github.com/werf/nelm/pkg/helm/pkg/release" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" -) - -func upgradeAction(t *testing.T) *Upgrade { - config := actionConfigFixture(t) - upAction := NewUpgrade(config, UpgradeOptions{}) - upAction.Namespace = "spaced" - - return upAction -} - -func TestUpgradeRelease_Success(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "previous-release" - rel.Info.Status = release.StatusDeployed - req.NoError(upAction.cfg.Releases.Create(rel)) - - upAction.Wait = true - vals := map[string]interface{}{} - - ctx, done := context.WithCancel(context.Background()) - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) - done() - req.NoError(err) - is.Equal(res.Info.Status, release.StatusDeployed) - - // Detecting previous bug where context termination after successful release - // caused release to fail. - time.Sleep(time.Millisecond * 100) - lastRelease, err := upAction.cfg.Releases.Last(rel.Name) - req.NoError(err) - is.Equal(lastRelease.Info.Status, release.StatusDeployed) -} - -func TestUpgradeRelease_Wait(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - upAction.cfg.KubeClient = failer - upAction.Wait = true - vals := map[string]interface{}{} - - res, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} - -func TestUpgradeRelease_WaitForJobs(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - upAction.cfg.KubeClient = failer - upAction.Wait = true - upAction.WaitForJobs = true - vals := map[string]interface{}{} - - res, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} - -func TestUpgradeRelease_CleanupOnFail(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitError = fmt.Errorf("I timed out") - failer.DeleteError = fmt.Errorf("I tried to delete nil") - upAction.cfg.KubeClient = failer - upAction.Wait = true - upAction.CleanupOnFail = true - vals := map[string]interface{}{} - - res, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.NotContains(err.Error(), "unable to cleanup resources") - is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) -} - -func TestUpgradeRelease_Atomic(t *testing.T) { - is := assert.New(t) - req := require.New(t) - - t.Run("atomic rollback succeeds", func(t *testing.T) { - upAction := upgradeAction(t) - - rel := releaseStub() - rel.Name = "nuketown" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - // We can't make Update error because then the rollback won't work - failer.WatchUntilReadyError = fmt.Errorf("arming key removed") - upAction.cfg.KubeClient = failer - upAction.Atomic = true - vals := map[string]interface{}{} - - res, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.Contains(err.Error(), "arming key removed") - is.Contains(err.Error(), "atomic") - - // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) - is.NoError(err) - // Should have rolled back to the previous - is.Equal(updatedRes.Info.Status, release.StatusDeployed) - }) - - t.Run("atomic uninstall fails", func(t *testing.T) { - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "fallout" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.UpdateError = fmt.Errorf("update fail") - upAction.cfg.KubeClient = failer - upAction.Atomic = true - vals := map[string]interface{}{} - - _, err := upAction.Run(rel.Name, buildChart(), vals) - req.Error(err) - is.Contains(err.Error(), "update fail") - is.Contains(err.Error(), "an error occurred while rolling back the release") - }) -} - -func TestUpgradeRelease_ReuseValues(t *testing.T) { - is := assert.New(t) - - t.Run("reuse values should work with values", func(t *testing.T) { - upAction := upgradeAction(t) - - existingValues := map[string]interface{}{ - "name": "value", - "maxHeapSize": "128m", - "replicas": 2, - } - newValues := map[string]interface{}{ - "name": "newValue", - "maxHeapSize": "512m", - "cpu": "12m", - } - expectedValues := map[string]interface{}{ - "name": "newValue", - "maxHeapSize": "512m", - "cpu": "12m", - "replicas": 2, - } - - rel := releaseStub() - rel.Name = "nuketown" - rel.Info.Status = release.StatusDeployed - rel.Config = existingValues - - err := upAction.cfg.Releases.Create(rel) - is.NoError(err) - - upAction.ReuseValues = true - // setting newValues and upgrading - res, err := upAction.Run(rel.Name, buildChart(), newValues) - is.NoError(err) - - // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) - is.NoError(err) - - if updatedRes == nil { - is.Fail("Updated Release is nil") - return - } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) - is.Equal(expectedValues, updatedRes.Config) - }) - - t.Run("reuse values should not install disabled charts", func(t *testing.T) { - upAction := upgradeAction(t) - chartDefaultValues := map[string]interface{}{ - "subchart": map[string]interface{}{ - "enabled": true, - }, - } - dependency := chart.Dependency{ - Name: "subchart", - Version: "0.1.0", - Repository: "http://some-repo.com", - Condition: "subchart.enabled", - } - sampleChart := buildChart( - withName("sample"), - withValues(chartDefaultValues), - withMetadataDependency(dependency), - ) - now := helmtime.Now() - existingValues := map[string]interface{}{ - "subchart": map[string]interface{}{ - "enabled": false, - }, - } - rel := &release.Release{ - Name: "nuketown", - Info: &release.Info{ - FirstDeployed: now, - LastDeployed: now, - Status: release.StatusDeployed, - Description: "Named Release Stub", - }, - Chart: sampleChart, - Config: existingValues, - Version: 1, - } - err := upAction.cfg.Releases.Create(rel) - is.NoError(err) - - upAction.ReuseValues = true - sampleChartWithSubChart := buildChart( - withName(sampleChart.Name()), - withValues(sampleChart.Values), - withDependency(withName("subchart")), - withMetadataDependency(dependency), - ) - // reusing values and upgrading - res, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{}) - is.NoError(err) - - // Now get the upgraded release - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) - is.NoError(err) - - if updatedRes == nil { - is.Fail("Updated Release is nil") - return - } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) - is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies") - - expectedValues := map[string]interface{}{ - "subchart": map[string]interface{}{ - "enabled": false, - }, - } - is.Equal(expectedValues, updatedRes.Config) - }) -} - -func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) { - is := assert.New(t) - - t.Run("reset then reuse values should work with values", func(t *testing.T) { - upAction := upgradeAction(t) - - existingValues := map[string]interface{}{ - "name": "value", - "maxHeapSize": "128m", - "replicas": 2, - } - newValues := map[string]interface{}{ - "name": "newValue", - "maxHeapSize": "512m", - "cpu": "12m", - } - newChartValues := map[string]interface{}{ - "memory": "256m", - } - expectedValues := map[string]interface{}{ - "name": "newValue", - "maxHeapSize": "512m", - "cpu": "12m", - "replicas": 2, - } - - rel := releaseStub() - rel.Name = "nuketown" - rel.Info.Status = release.StatusDeployed - rel.Config = existingValues - - err := upAction.cfg.Releases.Create(rel) - is.NoError(err) - - upAction.ResetThenReuseValues = true - // setting newValues and upgrading - res, err := upAction.Run(rel.Name, buildChart(withValues(newChartValues)), newValues) - is.NoError(err) - - // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) - is.NoError(err) - - if updatedRes == nil { - is.Fail("Updated Release is nil") - return - } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) - is.Equal(expectedValues, updatedRes.Config) - is.Equal(newChartValues, updatedRes.Chart.Values) - }) -} - -func TestUpgradeRelease_Pending(t *testing.T) { - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - rel2 := releaseStub() - rel2.Name = "come-fail-away" - rel2.Info.Status = release.StatusPendingUpgrade - rel2.Version = 2 - upAction.cfg.Releases.Create(rel2) - - vals := map[string]interface{}{} - - _, err := upAction.Run(rel.Name, buildChart(), vals) - req.Contains(err.Error(), "progress", err) -} - -func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { - - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "interrupted-release" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 10 * time.Second - upAction.cfg.KubeClient = failer - upAction.Wait = true - vals := map[string]interface{}{} - - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - time.AfterFunc(time.Second, cancel) - - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) - - req.Error(err) - is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled") - is.Equal(res.Info.Status, release.StatusFailed) - -} - -func TestUpgradeRelease_Interrupted_Atomic(t *testing.T) { - - is := assert.New(t) - req := require.New(t) - - upAction := upgradeAction(t) - rel := releaseStub() - rel.Name = "interrupted-release" - rel.Info.Status = release.StatusDeployed - upAction.cfg.Releases.Create(rel) - - failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.WaitDuration = 5 * time.Second - upAction.cfg.KubeClient = failer - upAction.Atomic = true - vals := map[string]interface{}{} - - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - time.AfterFunc(time.Second, cancel) - - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) - - req.Error(err) - is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to atomic being set: context canceled") - - // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) - is.NoError(err) - // Should have rolled back to the previous - is.Equal(updatedRes.Info.Status, release.StatusDeployed) -} - -func TestMergeCustomLabels(t *testing.T) { - var tests = [][3]map[string]string{ - {nil, nil, map[string]string{}}, - {map[string]string{}, map[string]string{}, map[string]string{}}, - {map[string]string{"k1": "v1", "k2": "v2"}, nil, map[string]string{"k1": "v1", "k2": "v2"}}, - {nil, map[string]string{"k1": "v1", "k2": "v2"}, map[string]string{"k1": "v1", "k2": "v2"}}, - {map[string]string{"k1": "v1", "k2": "v2"}, map[string]string{"k1": "null", "k2": "v3"}, map[string]string{"k2": "v3"}}, - } - for _, test := range tests { - if output := mergeCustomLabels(test[0], test[1]); !reflect.DeepEqual(test[2], output) { - t.Errorf("Expected {%v}, got {%v}", test[2], output) - } - } -} - -func TestUpgradeRelease_Labels(t *testing.T) { - is := assert.New(t) - upAction := upgradeAction(t) - - rel := releaseStub() - rel.Name = "labels" - // It's needed to check that suppressed release would keep original labels - rel.Labels = map[string]string{ - "key1": "val1", - "key2": "val2.1", - } - rel.Info.Status = release.StatusDeployed - - err := upAction.cfg.Releases.Create(rel) - is.NoError(err) - - upAction.Labels = map[string]string{ - "key1": "null", - "key2": "val2.2", - "key3": "val3", - } - // setting newValues and upgrading - res, err := upAction.Run(rel.Name, buildChart(), nil) - is.NoError(err) - - // Now make sure it is actually upgraded and labels were merged - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) - is.NoError(err) - - if updatedRes == nil { - is.Fail("Updated Release is nil") - return - } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) - is.Equal(mergeCustomLabels(rel.Labels, upAction.Labels), updatedRes.Labels) - - // Now make sure it is suppressed release still contains original labels - initialRes, err := upAction.cfg.Releases.Get(res.Name, 1) - is.NoError(err) - - if initialRes == nil { - is.Fail("Updated Release is nil") - return - } - is.Equal(initialRes.Info.Status, release.StatusSuperseded) - is.Equal(initialRes.Labels, rel.Labels) -} - -func TestUpgradeRelease_SystemLabels(t *testing.T) { - is := assert.New(t) - upAction := upgradeAction(t) - - rel := releaseStub() - rel.Name = "labels" - // It's needed to check that suppressed release would keep original labels - rel.Labels = map[string]string{ - "key1": "val1", - "key2": "val2.1", - } - rel.Info.Status = release.StatusDeployed - - err := upAction.cfg.Releases.Create(rel) - is.NoError(err) - - upAction.Labels = map[string]string{ - "key1": "null", - "key2": "val2.2", - "owner": "val3", - } - // setting newValues and upgrading - _, err = upAction.Run(rel.Name, buildChart(), nil) - if err == nil { - t.Fatal("expected an error") - } - - is.Equal(fmt.Errorf("user suplied labels contains system reserved label name. System labels: %+v", driver.GetSystemLabels()), err) -} diff --git a/pkg/helm/pkg/action/validate.go b/pkg/helm/pkg/action/validate.go deleted file mode 100644 index 945691fa..00000000 --- a/pkg/helm/pkg/action/validate.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package action - -import ( - "fmt" - - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/cli-runtime/pkg/resource" -) - -func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) { - var requireUpdate kube.ResourceList - - err := resources.Visit(func(info *resource.Info, err error) error { - if err != nil { - return err - } - - helper := resource.NewHelper(info.Client, info.Mapping) - existing, err := helper.Get(info.Namespace, info.Name) - if err != nil { - if apierrors.IsNotFound(err) { - return nil - } - return errors.Wrapf(err, "could not get information about the resource %s", releaseutil.ResourceString(info)) - } - - // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. - if err := releaseutil.CheckOwnership(existing, releaseName, releaseNamespace); err != nil { - return fmt.Errorf("%s exists and cannot be imported into the current release: %s", releaseutil.ResourceString(info), err) - } - - requireUpdate.Append(info) - return nil - }) - - return requireUpdate, err -} - -func ExistingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) { - return existingResourceConflict(resources, releaseName, releaseNamespace) -} diff --git a/pkg/helm/pkg/action/verify.go b/pkg/helm/pkg/action/verify.go index 18301939..04454a03 100644 --- a/pkg/helm/pkg/action/verify.go +++ b/pkg/helm/pkg/action/verify.go @@ -28,7 +28,6 @@ import ( // It provides the implementation of 'helm verify'. type Verify struct { Keyring string - Out string } // NewVerify creates a new Verify object with the given configuration. @@ -37,23 +36,18 @@ func NewVerify() *Verify { } // Run executes 'helm verify'. -func (v *Verify) Run(chartfile string) error { +func (v *Verify) Run(chartfile string) (string, error) { var out strings.Builder - p, err := downloader.VerifyChart(chartfile, v.Keyring) + p, err := downloader.VerifyChart(chartfile, chartfile+".prov", v.Keyring) if err != nil { - return err + return "", err } for name := range p.SignedBy.Identities { - fmt.Fprintf(&out, "Signed by: %v\n", name) + _, _ = fmt.Fprintf(&out, "Signed by: %v\n", name) } - fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint) - fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash) + _, _ = fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint) + _, _ = fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash) - // TODO(mattfarina): The output is set as a property rather than returned - // to maintain the Go API. In Helm v4 this function should return the out - // and the property on the struct can be removed. - v.Out = out.String() - - return nil + return out.String(), err } diff --git a/pkg/helm/pkg/action/verify_test.go b/pkg/helm/pkg/action/verify_test.go new file mode 100644 index 00000000..343dacae --- /dev/null +++ b/pkg/helm/pkg/action/verify_test.go @@ -0,0 +1,48 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewVerify(t *testing.T) { + client := NewVerify() + + assert.NotNil(t, client) +} + +func TestVerifyRun(t *testing.T) { + client := NewVerify() + + client.Keyring = "../downloader/testdata/helm-test-key.pub" + output, err := client.Run("../downloader/testdata/signtest-0.1.0.tgz") + assert.Contains(t, output, "Signed by:") + assert.Contains(t, output, "Using Key With Fingerprint:") + assert.Contains(t, output, "Chart Hash Verified:") + require.NoError(t, err) +} + +func TestVerifyRun_DownloadError(t *testing.T) { + client := NewVerify() + output, err := client.Run("invalid-chart-path") + require.Error(t, err) + assert.Empty(t, output) +} diff --git a/pkg/helm/pkg/chart/chart.go b/pkg/helm/pkg/chart/chart.go deleted file mode 100644 index 2e4de1fa..00000000 --- a/pkg/helm/pkg/chart/chart.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chart - -import ( - "path/filepath" - "regexp" - "strings" - - "github.com/samber/lo" - - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/runtimedata" -) - -// APIVersionV1 is the API version number for version 1. -const APIVersionV1 = "v1" - -// APIVersionV2 is the API version number for version 2. -const APIVersionV2 = "v2" - -// aliasNameFormat defines the characters that are legal in an alias name. -var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$") - -// Chart is a helm package that contains metadata, a default config, zero or more -// optionally parameterizable templates, and zero or more charts (dependencies). -type Chart struct { - // Raw contains the raw contents of the files originally contained in the chart archive. - // - // This should not be used except in special cases like `helm show values`, - // where we want to display the raw values, comments and all. - Raw []*File `json:"-" copy:"shallow"` - // Metadata is the contents of the Chartfile. - Metadata *Metadata `json:"metadata"` - // Lock is the contents of Chart.lock. - Lock *Lock `json:"lock"` - // Templates for this chart. - Templates []*File `json:"templates" copy:"shallow"` - // Values are default config for this chart. - Values map[string]interface{} `json:"values"` - // Schema is an optional JSON schema for imposing structure on Values - Schema []byte `json:"schema"` - // Files are miscellaneous files in a chart archive, - // e.g. README, LICENSE, etc. - Files []*File `json:"files" copy:"shallow"` - // Files that are used at runtime, but should not be saved to secret/configmap. - RuntimeFiles []*File `json:"-" copy:"shallow"` - - parent *Chart - dependencies []*Chart - - SecretsRuntimeData runtimedata.RuntimeData `json:"-"` - ExtraValues map[string]interface{} `json:"-"` -} - -type CRD struct { - // Name is the File.Name for the crd file - Name string - // Filename is the File obj Name including (sub-)chart.ChartFullPath - Filename string - // File is the File obj for the crd - File *File -} - -// SetDependencies replaces the chart dependencies. -func (ch *Chart) SetDependencies(charts ...*Chart) { - ch.dependencies = nil - ch.AddDependency(charts...) -} - -// Name returns the name of the chart. -func (ch *Chart) Name() string { - if ch.Metadata == nil { - return "" - } - return ch.Metadata.Name -} - -// AddDependency determines if the chart is a subchart. -func (ch *Chart) AddDependency(charts ...*Chart) { - for i, x := range charts { - charts[i].parent = ch - ch.dependencies = append(ch.dependencies, x) - } -} - -// Root finds the root chart. -func (ch *Chart) Root() *Chart { - if ch.IsRoot() { - return ch - } - return ch.Parent().Root() -} - -// Dependencies are the charts that this chart depends on. -func (ch *Chart) Dependencies() []*Chart { return ch.dependencies } - -// IsRoot determines if the chart is the root chart. -func (ch *Chart) IsRoot() bool { return ch.parent == nil } - -// Parent returns a subchart's parent chart. -func (ch *Chart) Parent() *Chart { return ch.parent } - -// ChartPath returns the full path to this chart in dot notation. -func (ch *Chart) ChartPath() string { - if !ch.IsRoot() { - return ch.Parent().ChartPath() + "." + ch.Name() - } - return ch.Name() -} - -// ChartFullPath returns the full path to this chart. -func (ch *Chart) ChartFullPath() string { - if !ch.IsRoot() { - return ch.Parent().ChartFullPath() + "/charts/" + ch.Name() - } - return ch.Name() -} - -// Validate validates the metadata. -func (ch *Chart) Validate() error { - return ch.Metadata.Validate() -} - -// AppVersion returns the appversion of the chart. -func (ch *Chart) AppVersion() string { - if ch.Metadata == nil { - return "" - } - return ch.Metadata.AppVersion -} - -// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. -// Deprecated: use CRDObjects() -func (ch *Chart) CRDs() []*File { - files := []*File{} - // Find all resources in the crds/ directory - for _, f := range ch.Files { - if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { - files = append(files, f) - } - } - // Get CRDs from dependencies, too. - for _, dep := range ch.Dependencies() { - files = append(files, dep.CRDs()...) - } - return files -} - -// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts -func (ch *Chart) CRDObjects() []CRD { - crds := []CRD{} - // Find all resources in the crds/ directory - for _, f := range ch.Files { - if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { - mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f} - crds = append(crds, mycrd) - } - } - // Get CRDs from dependencies, too. - for _, dep := range ch.Dependencies() { - crds = append(crds, dep.CRDObjects()...) - } - return crds -} - -func hasManifestExtension(fname string) bool { - ext := filepath.Ext(fname) - return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json") -} - -func (ch *Chart) AddRuntimeFile(name string, data []byte) { - ch.Raw = append(ch.Raw, &File{Name: name, Data: data}) - - ch.RuntimeFiles = append(ch.RuntimeFiles, &File{Name: name, Data: data}) - if !ch.IsRoot() { - root := ch.Root() - rawName := getRootRawFileName(ch, name) - root.Raw = append(root.Raw, &File{Name: rawName, Data: data}) - } -} - -func (ch *Chart) RemoveRuntimeFile(name string) { - ch.Raw = lo.Reject(ch.Raw, func(f *File, _ int) bool { - return f.Name == name - }) - - ch.RuntimeFiles = lo.Reject(ch.RuntimeFiles, func(f *File, _ int) bool { - return f.Name == name - }) - - if !ch.IsRoot() { - root := ch.Root() - rawName := getRootRawFileName(ch, name) - root.Raw = lo.Reject(root.Raw, func(f *File, _ int) bool { - return f.Name == rawName - }) - } -} - -func getRootRawFileName(ch *Chart, name string) string { - return filepath.Join(strings.TrimPrefix(ch.ChartFullPath(), ch.Root().Name()+"/"), name) -} diff --git a/pkg/helm/pkg/chart/chart_test.go b/pkg/helm/pkg/chart/chart_test.go deleted file mode 100644 index 62d60765..00000000 --- a/pkg/helm/pkg/chart/chart_test.go +++ /dev/null @@ -1,211 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package chart - -import ( - "encoding/json" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCRDs(t *testing.T) { - chrt := Chart{ - Files: []*File{ - { - Name: "crds/foo.yaml", - Data: []byte("hello"), - }, - { - Name: "bar.yaml", - Data: []byte("hello"), - }, - { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), - }, - { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), - }, - { - Name: "crds/README.md", - Data: []byte("# hello"), - }, - }, - } - - is := assert.New(t) - crds := chrt.CRDs() - is.Equal(2, len(crds)) - is.Equal("crds/foo.yaml", crds[0].Name) - is.Equal("crds/foo/bar/baz.yaml", crds[1].Name) -} - -func TestSaveChartNoRawData(t *testing.T) { - chrt := Chart{ - Raw: []*File{ - { - Name: "fhqwhgads.yaml", - Data: []byte("Everybody to the Limit"), - }, - }, - } - - is := assert.New(t) - data, err := json.Marshal(chrt) - if err != nil { - t.Fatal(err) - } - - res := &Chart{} - if err := json.Unmarshal(data, res); err != nil { - t.Fatal(err) - } - - is.Equal([]*File(nil), res.Raw) -} - -func TestMetadata(t *testing.T) { - chrt := Chart{ - Metadata: &Metadata{ - Name: "foo.yaml", - AppVersion: "1.0.0", - APIVersion: "v2", - Version: "1.0.0", - Type: "application", - }, - } - - is := assert.New(t) - - is.Equal("foo.yaml", chrt.Name()) - is.Equal("1.0.0", chrt.AppVersion()) - is.Equal(nil, chrt.Validate()) -} - -func TestIsRoot(t *testing.T) { - chrt1 := Chart{ - parent: &Chart{ - Metadata: &Metadata{ - Name: "foo", - }, - }, - } - - chrt2 := Chart{ - Metadata: &Metadata{ - Name: "foo", - }, - } - - is := assert.New(t) - - is.Equal(false, chrt1.IsRoot()) - is.Equal(true, chrt2.IsRoot()) -} - -func TestChartPath(t *testing.T) { - chrt1 := Chart{ - parent: &Chart{ - Metadata: &Metadata{ - Name: "foo", - }, - }, - } - - chrt2 := Chart{ - Metadata: &Metadata{ - Name: "foo", - }, - } - - is := assert.New(t) - - is.Equal("foo.", chrt1.ChartPath()) - is.Equal("foo", chrt2.ChartPath()) -} - -func TestChartFullPath(t *testing.T) { - chrt1 := Chart{ - parent: &Chart{ - Metadata: &Metadata{ - Name: "foo", - }, - }, - } - - chrt2 := Chart{ - Metadata: &Metadata{ - Name: "foo", - }, - } - - is := assert.New(t) - - is.Equal("foo/charts/", chrt1.ChartFullPath()) - is.Equal("foo", chrt2.ChartFullPath()) -} - -func TestCRDObjects(t *testing.T) { - chrt := Chart{ - Files: []*File{ - { - Name: "crds/foo.yaml", - Data: []byte("hello"), - }, - { - Name: "bar.yaml", - Data: []byte("hello"), - }, - { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), - }, - { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), - }, - { - Name: "crds/README.md", - Data: []byte("# hello"), - }, - }, - } - - expected := []CRD{ - { - Name: "crds/foo.yaml", - Filename: "crds/foo.yaml", - File: &File{ - Name: "crds/foo.yaml", - Data: []byte("hello"), - }, - }, - { - Name: "crds/foo/bar/baz.yaml", - Filename: "crds/foo/bar/baz.yaml", - File: &File{ - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), - }, - }, - } - - is := assert.New(t) - crds := chrt.CRDObjects() - is.Equal(expected, crds) -} diff --git a/pkg/helm/pkg/chart/common.go b/pkg/helm/pkg/chart/common.go new file mode 100644 index 00000000..455a9066 --- /dev/null +++ b/pkg/helm/pkg/chart/common.go @@ -0,0 +1,243 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chart + +import ( + "errors" + "fmt" + "log/slog" + "reflect" + "strings" + + v3chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + common "github.com/werf/nelm/pkg/helm/pkg/chart/common" + v2chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +var NewAccessor func(chrt Charter) (Accessor, error) = NewDefaultAccessor //nolint:revive + +func NewDefaultAccessor(chrt Charter) (Accessor, error) { + switch v := chrt.(type) { + case v2chart.Chart: + return &v2Accessor{&v}, nil + case *v2chart.Chart: + return &v2Accessor{v}, nil + case v3chart.Chart: + return &v3Accessor{&v}, nil + case *v3chart.Chart: + return &v3Accessor{v}, nil + default: + return nil, errors.New("unsupported chart type") + } +} + +type v2Accessor struct { + chrt *v2chart.Chart +} + +func (r *v2Accessor) Name() string { + return r.chrt.Metadata.Name +} + +func (r *v2Accessor) IsRoot() bool { + return r.chrt.IsRoot() +} + +func (r *v2Accessor) MetadataAsMap() map[string]interface{} { + var ret map[string]interface{} + if r.chrt.Metadata == nil { + return ret + } + + ret, err := structToMap(r.chrt.Metadata) + if err != nil { + slog.Error("error converting metadata to map", "error", err) + } + return ret +} + +func (r *v2Accessor) Files() []*common.File { + return r.chrt.Files +} + +func (r *v2Accessor) Templates() []*common.File { + return r.chrt.Templates +} + +func (r *v2Accessor) ChartFullPath() string { + return r.chrt.ChartFullPath() +} + +func (r *v2Accessor) IsLibraryChart() bool { + return strings.EqualFold(r.chrt.Metadata.Type, "library") +} + +func (r *v2Accessor) Dependencies() []Charter { + var deps = make([]Charter, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Dependencies() { + deps[i] = c + } + return deps +} + +func (r *v2Accessor) MetaDependencies() []Dependency { + var deps = make([]Dependency, len(r.chrt.Metadata.Dependencies)) + for i, c := range r.chrt.Metadata.Dependencies { + deps[i] = c + } + return deps +} + +func (r *v2Accessor) Values() map[string]interface{} { + return r.chrt.Values +} + +func (r *v2Accessor) Schema() []byte { + return r.chrt.Schema +} + +func (r *v2Accessor) Deprecated() bool { + return r.chrt.Metadata.Deprecated +} + +type v3Accessor struct { + chrt *v3chart.Chart +} + +func (r *v3Accessor) Name() string { + return r.chrt.Metadata.Name +} + +func (r *v3Accessor) IsRoot() bool { + return r.chrt.IsRoot() +} + +func (r *v3Accessor) MetadataAsMap() map[string]interface{} { + var ret map[string]interface{} + if r.chrt.Metadata == nil { + return ret + } + + ret, err := structToMap(r.chrt.Metadata) + if err != nil { + slog.Error("error converting metadata to map", "error", err) + } + return ret +} + +func (r *v3Accessor) Files() []*common.File { + return r.chrt.Files +} + +func (r *v3Accessor) Templates() []*common.File { + return r.chrt.Templates +} + +func (r *v3Accessor) ChartFullPath() string { + return r.chrt.ChartFullPath() +} + +func (r *v3Accessor) IsLibraryChart() bool { + return strings.EqualFold(r.chrt.Metadata.Type, "library") +} + +func (r *v3Accessor) Dependencies() []Charter { + var deps = make([]Charter, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Dependencies() { + deps[i] = c + } + return deps +} + +func (r *v3Accessor) MetaDependencies() []Dependency { + var deps = make([]Dependency, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Metadata.Dependencies { + deps[i] = c + } + return deps +} + +func (r *v3Accessor) Values() map[string]interface{} { + return r.chrt.Values +} + +func (r *v3Accessor) Schema() []byte { + return r.chrt.Schema +} + +func (r *v3Accessor) Deprecated() bool { + return r.chrt.Metadata.Deprecated +} + +func structToMap(obj interface{}) (map[string]interface{}, error) { + objValue := reflect.ValueOf(obj) + + // If the value is a pointer, dereference it + if objValue.Kind() == reflect.Pointer { + objValue = objValue.Elem() + } + + // Check if the input is a struct + if objValue.Kind() != reflect.Struct { + return nil, fmt.Errorf("input must be a struct or a pointer to a struct") + } + + result := make(map[string]interface{}) + objType := objValue.Type() + + for i := 0; i < objValue.NumField(); i++ { + field := objType.Field(i) + value := objValue.Field(i) + + switch value.Kind() { + case reflect.Struct: + nestedMap, err := structToMap(value.Interface()) + if err != nil { + return nil, err + } + result[field.Name] = nestedMap + case reflect.Pointer: + // Recurse for pointers by dereferencing + if value.IsNil() { + result[field.Name] = nil + } else { + nestedMap, err := structToMap(value.Interface()) + if err != nil { + return nil, err + } + result[field.Name] = nestedMap + } + case reflect.Slice: + sliceOfMaps := make([]interface{}, value.Len()) + for j := 0; j < value.Len(); j++ { + sliceElement := value.Index(j) + if sliceElement.Kind() == reflect.Struct || sliceElement.Kind() == reflect.Pointer { + nestedMap, err := structToMap(sliceElement.Interface()) + if err != nil { + return nil, err + } + sliceOfMaps[j] = nestedMap + } else { + sliceOfMaps[j] = sliceElement.Interface() + } + } + result[field.Name] = sliceOfMaps + default: + result[field.Name] = value.Interface() + } + } + return result, nil +} diff --git a/pkg/helm/pkg/chart/common/capabilities.go b/pkg/helm/pkg/chart/common/capabilities.go new file mode 100644 index 00000000..375da741 --- /dev/null +++ b/pkg/helm/pkg/chart/common/capabilities.go @@ -0,0 +1,182 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "fmt" + "slices" + "strconv" + "strings" + "testing" + + "github.com/Masterminds/semver/v3" + "k8s.io/client-go/kubernetes/scheme" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + k8sversion "k8s.io/apimachinery/pkg/util/version" + + helmversion "github.com/werf/nelm/pkg/helm/intern/version" +) + +const ( + kubeVersionMajorTesting = 1 + kubeVersionMinorTesting = 20 +) + +var ( + // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). + DefaultVersionSet = allKnownVersions() + + DefaultCapabilities = func() *Capabilities { + caps, err := makeDefaultCapabilities() + if err != nil { + panic(fmt.Sprintf("failed to create default capabilities: %v", err)) + } + return caps + + }() +) + +// Capabilities describes the capabilities of the Kubernetes cluster. +type Capabilities struct { + // KubeVersion is the Kubernetes version. + KubeVersion KubeVersion + // APIVersions are supported Kubernetes API versions. + APIVersions VersionSet + // HelmVersion is the build information for this helm version + HelmVersion helmversion.BuildInfo +} + +func (capabilities *Capabilities) Copy() *Capabilities { + return &Capabilities{ + KubeVersion: capabilities.KubeVersion, + APIVersions: capabilities.APIVersions, + HelmVersion: capabilities.HelmVersion, + } +} + +// KubeVersion is the Kubernetes version. +type KubeVersion struct { + Version string // Full version (e.g., v1.33.4-gke.1245000) + normalizedVersion string // Normalized for constraint checking (e.g., v1.33.4) + Major string // Kubernetes major version + Minor string // Kubernetes minor version +} + +// String implements fmt.Stringer. +// Returns the normalized version used for constraint checking. +func (kv *KubeVersion) String() string { + if kv.normalizedVersion != "" { + return kv.normalizedVersion + } + return kv.Version +} + +// GitVersion returns the full Kubernetes version string. +// +// Deprecated: use KubeVersion.Version. +func (kv *KubeVersion) GitVersion() string { return kv.Version } + +// ParseKubeVersion parses kubernetes version from string +func ParseKubeVersion(version string) (*KubeVersion, error) { + // Based on the original k8s version parser. + // https://github.com/kubernetes/kubernetes/blob/b266ac2c3e42c2c4843f81e20213d2b2f43e450a/staging/src/k8s.io/apimachinery/pkg/util/version/version.go#L137 + sv, err := k8sversion.ParseGeneric(version) + if err != nil { + return nil, err + } + + // Preserve original input (e.g., v1.33.4-gke.1245000) + gitVersion := version + if !strings.HasPrefix(version, "v") { + gitVersion = "v" + version + } + + // Normalize for constraint checking (strips all suffixes) + normalizedVer := "v" + sv.String() + + return &KubeVersion{ + Version: gitVersion, + normalizedVersion: normalizedVer, + Major: strconv.FormatUint(uint64(sv.Major()), 10), + Minor: strconv.FormatUint(uint64(sv.Minor()), 10), + }, nil +} + +// VersionSet is a set of Kubernetes API versions. +type VersionSet []string + +// Has returns true if the version string is in the set. +// +// vs.Has("apps/v1") +func (v VersionSet) Has(apiVersion string) bool { + return slices.Contains(v, apiVersion) +} + +func allKnownVersions() VersionSet { + // We should register the built in extension APIs as well so CRDs are + // supported in the default version set. This has caused problems with `helm + // template` in the past, so let's be safe + apiextensionsv1beta1.AddToScheme(scheme.Scheme) + apiextensionsv1.AddToScheme(scheme.Scheme) + + groups := scheme.Scheme.PrioritizedVersionsAllGroups() + vs := make(VersionSet, 0, len(groups)) + for _, gv := range groups { + vs = append(vs, gv.String()) + } + return vs +} + +func makeDefaultCapabilities() (*Capabilities, error) { + // Test builds don't include debug info / module info + // (And even if they did, we probably want stable capabilities for tests anyway) + // Return a default value for test builds + if testing.Testing() { + return newCapabilities(kubeVersionMajorTesting, kubeVersionMinorTesting) + } + + vstr, err := helmversion.K8sIOClientGoModVersion() + if err != nil { + return nil, fmt.Errorf("failed to retrieve k8s.io/client-go version: %w", err) + } + + v, err := semver.NewVersion(vstr) + if err != nil { + return nil, fmt.Errorf("unable to parse k8s.io/client-go version %q: %v", vstr, err) + } + + kubeVersionMajor := v.Major() + 1 + kubeVersionMinor := v.Minor() + + return newCapabilities(kubeVersionMajor, kubeVersionMinor) +} + +func newCapabilities(kubeVersionMajor, kubeVersionMinor uint64) (*Capabilities, error) { + + version := fmt.Sprintf("v%d.%d.0", kubeVersionMajor, kubeVersionMinor) + return &Capabilities{ + KubeVersion: KubeVersion{ + Version: version, + normalizedVersion: version, + Major: fmt.Sprintf("%d", kubeVersionMajor), + Minor: fmt.Sprintf("%d", kubeVersionMinor), + }, + APIVersions: DefaultVersionSet, + HelmVersion: helmversion.Get(), + }, nil +} diff --git a/pkg/helm/pkg/chart/common/capabilities_test.go b/pkg/helm/pkg/chart/common/capabilities_test.go new file mode 100644 index 00000000..b96d7d29 --- /dev/null +++ b/pkg/helm/pkg/chart/common/capabilities_test.go @@ -0,0 +1,121 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "testing" +) + +func TestVersionSet(t *testing.T) { + vs := VersionSet{"v1", "apps/v1"} + if d := len(vs); d != 2 { + t.Errorf("Expected 2 versions, got %d", d) + } + + if !vs.Has("apps/v1") { + t.Error("Expected to find apps/v1") + } + + if vs.Has("Spanish/inquisition") { + t.Error("No one expects the Spanish/inquisition") + } +} + +func TestDefaultVersionSet(t *testing.T) { + if !DefaultVersionSet.Has("v1") { + t.Error("Expected core v1 version set") + } +} + +func TestDefaultCapabilities(t *testing.T) { + caps := DefaultCapabilities + kv := caps.KubeVersion + if kv.String() != "v1.20.0" { + t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String()) + } + if kv.Version != "v1.20.0" { + t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version) + } + if kv.GitVersion() != "v1.20.0" { + t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version) + } + if kv.Major != "1" { + t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major) + } + if kv.Minor != "20" { + t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) + } + + hv := caps.HelmVersion + if hv.Version != "v4.1" { + t.Errorf("Expected default HelmVersion to be v4.1, got %q", hv.Version) + } +} + +func TestParseKubeVersion(t *testing.T) { + kv, err := ParseKubeVersion("v1.16.0") + if err != nil { + t.Errorf("Expected v1.16.0 to parse successfully") + } + if kv.Version != "v1.16.0" { + t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String()) + } + if kv.Major != "1" { + t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major) + } + if kv.Minor != "16" { + t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor) + } +} + +func TestParseKubeVersionWithVendorSuffixes(t *testing.T) { + tests := []struct { + name string + input string + wantVer string + wantString string + wantMajor string + wantMinor string + }{ + {"GKE vendor suffix", "v1.33.4-gke.1245000", "v1.33.4-gke.1245000", "v1.33.4", "1", "33"}, + {"GKE without v", "1.30.2-gke.1587003", "v1.30.2-gke.1587003", "v1.30.2", "1", "30"}, + {"EKS trailing +", "v1.28+", "v1.28+", "v1.28", "1", "28"}, + {"EKS + without v", "1.28+", "v1.28+", "v1.28", "1", "28"}, + {"Standard version", "v1.31.0", "v1.31.0", "v1.31.0", "1", "31"}, + {"Standard without v", "1.29.0", "v1.29.0", "v1.29.0", "1", "29"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kv, err := ParseKubeVersion(tt.input) + if err != nil { + t.Fatalf("ParseKubeVersion() error = %v", err) + } + if kv.Version != tt.wantVer { + t.Errorf("Version = %q, want %q", kv.Version, tt.wantVer) + } + if kv.String() != tt.wantString { + t.Errorf("String() = %q, want %q", kv.String(), tt.wantString) + } + if kv.Major != tt.wantMajor { + t.Errorf("Major = %q, want %q", kv.Major, tt.wantMajor) + } + if kv.Minor != tt.wantMinor { + t.Errorf("Minor = %q, want %q", kv.Minor, tt.wantMinor) + } + }) + } +} diff --git a/pkg/helm/pkg/chart/common/errors.go b/pkg/helm/pkg/chart/common/errors.go new file mode 100644 index 00000000..b0a2d650 --- /dev/null +++ b/pkg/helm/pkg/chart/common/errors.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "fmt" +) + +// ErrNoTable indicates that a chart does not have a matching table. +type ErrNoTable struct { + Key string +} + +func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) } + +// ErrNoValue indicates that Values does not contain a key with a value +type ErrNoValue struct { + Key string +} + +func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) } + +type ErrInvalidChartName struct { + Name string +} + +func (e ErrInvalidChartName) Error() string { + return fmt.Sprintf("%q is not a valid chart name", e.Name) +} diff --git a/pkg/helm/pkg/chartutil/errors_test.go b/pkg/helm/pkg/chart/common/errors_test.go similarity index 97% rename from pkg/helm/pkg/chartutil/errors_test.go rename to pkg/helm/pkg/chart/common/errors_test.go index 3f63e373..06b3b054 100644 --- a/pkg/helm/pkg/chartutil/errors_test.go +++ b/pkg/helm/pkg/chart/common/errors_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package chartutil +package common import ( "testing" diff --git a/pkg/helm/pkg/chart/common/file.go b/pkg/helm/pkg/chart/common/file.go new file mode 100644 index 00000000..1068bf45 --- /dev/null +++ b/pkg/helm/pkg/chart/common/file.go @@ -0,0 +1,31 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import "time" + +// File represents a file as a name/value pair. +// +// By convention, name is a relative path within the scope of the chart's +// base directory. +type File struct { + // Name is the path-like name of the template. + Name string `json:"name"` + // Data is the template as byte data. + Data []byte `json:"data"` + // ModTime is the file's mod-time + ModTime time.Time `json:"modtime,omitzero"` +} diff --git a/pkg/helm/pkg/chart/common/runtime_data.go b/pkg/helm/pkg/chart/common/runtime_data.go new file mode 100644 index 00000000..9f6f5929 --- /dev/null +++ b/pkg/helm/pkg/chart/common/runtime_data.go @@ -0,0 +1,22 @@ +package common + +import ( + "context" + + "github.com/werf/common-go/pkg/secrets_manager" + nelmcommon "github.com/werf/nelm/pkg/common" +) + +type RuntimeData interface { + DecodeAndLoadSecrets(ctx context.Context, loadedChartFiles []*nelmcommon.BufferedFile, secretsManager *secrets_manager.SecretsManager, opts DecodeAndLoadSecretsOptions) error + GetDecryptedSecretValues() map[string]interface{} + GetDecryptedSecretFilesData() map[string]string +} + +type DecodeAndLoadSecretsOptions struct { + CustomSecretValueFiles []string + LoadFromLocalFilesystem bool + NoDecryptSecrets bool + SecretsWorkingDir string + WithoutDefaultSecretValues bool +} diff --git a/pkg/helm/pkg/chart/common/testdata/coleridge.yaml b/pkg/helm/pkg/chart/common/testdata/coleridge.yaml new file mode 100644 index 00000000..b6579628 --- /dev/null +++ b/pkg/helm/pkg/chart/common/testdata/coleridge.yaml @@ -0,0 +1,12 @@ +poet: "Coleridge" +title: "Rime of the Ancient Mariner" +stanza: ["at", "length", "did", "cross", "an", "Albatross"] + +mariner: + with: "crossbow" + shot: "ALBATROSS" + +water: + water: + where: "everywhere" + nor: "any drop to drink" diff --git a/pkg/helm/pkg/chart/common/util/coalesce.go b/pkg/helm/pkg/chart/common/util/coalesce.go new file mode 100644 index 00000000..f754adfe --- /dev/null +++ b/pkg/helm/pkg/chart/common/util/coalesce.go @@ -0,0 +1,413 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "fmt" + "log" + "maps" + + nelmcommon "github.com/werf/nelm/pkg/common" + v3 "github.com/werf/nelm/pkg/helm/intern/chart/v3" + "github.com/werf/nelm/pkg/helm/intern/copystructure" + chart "github.com/werf/nelm/pkg/helm/pkg/chart" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + v2 "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func concatPrefix(a, b string) string { + if a == "" { + return b + } + return fmt.Sprintf("%s.%s", a, b) +} + +// CoalesceValues coalesces all of the values in a chart (and its subcharts). +// +// Values are coalesced together using the following rules: +// +// - Values in a higher level chart always override values in a lower-level +// dependency chart +// - Scalar values and arrays are replaced, maps are merged +// - A chart has access to all of the variables for it, as well as all of +// the values destined for its dependencies. +func CoalesceValues(chrt chart.Charter, vals map[string]interface{}) (common.Values, error) { + vals, err := makeValues(chrt, vals) + if err != nil { + return vals, err + } + + valsCopy, err := copyValues(vals) + if err != nil { + return vals, err + } + return coalesce(log.Printf, chrt, valsCopy, "", false) +} + +// MergeValues is used to merge the values in a chart and its subcharts. This +// is different from Coalescing as nil/null values are preserved. +// +// Values are coalesced together using the following rules: +// +// - Values in a higher level chart always override values in a lower-level +// dependency chart +// - Scalar values and arrays are replaced, maps are merged +// - A chart has access to all of the variables for it, as well as all of +// the values destined for its dependencies. +// +// Retaining Nils is useful when processes early in a Helm action or business +// logic need to retain them for when Coalescing will happen again later in the +// business logic. +func MergeValues(chrt chart.Charter, vals map[string]interface{}) (common.Values, error) { + vals, err := makeValues(chrt, vals) + if err != nil { + return vals, err + } + + valsCopy, err := copyValues(vals) + if err != nil { + return vals, err + } + return coalesce(log.Printf, chrt, valsCopy, "", true) +} + +func copyValues(vals map[string]interface{}) (common.Values, error) { + v, err := copystructure.Copy(vals) + if err != nil { + return vals, err + } + + valsCopy := v.(map[string]interface{}) + // if we have an empty map, make sure it is initialized + if valsCopy == nil { + valsCopy = make(map[string]interface{}) + } + + return valsCopy, nil +} + +type printFn func(format string, v ...interface{}) + +// coalesce coalesces the dest values and the chart values, giving priority to the dest values. +// +// This is a helper function for CoalesceValues and MergeValues. +// +// Note, the merge argument specifies whether this is being used by MergeValues +// or CoalesceValues. Coalescing removes null values and their keys in some +// situations while merging keeps the null values. +func coalesce(printf printFn, ch chart.Charter, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { + coalesceValues(printf, ch, dest, prefix, merge) + return coalesceDeps(printf, ch, dest, prefix, merge) +} + +// coalesceDeps coalesces the dependencies of the given chart. +func coalesceDeps(printf printFn, chrt chart.Charter, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { + ch, err := chart.NewAccessor(chrt) + if err != nil { + return dest, err + } + for _, subchart := range ch.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return dest, err + } + if c, ok := dest[sub.Name()]; !ok { + // If dest doesn't already have the key, create it. + dest[sub.Name()] = make(map[string]interface{}) + } else if !istable(c) { + return dest, fmt.Errorf("type mismatch on %s: %t", sub.Name(), c) + } + if dv, ok := dest[sub.Name()]; ok { + dvmap := dv.(map[string]interface{}) + subPrefix := concatPrefix(prefix, ch.Name()) + // Get globals out of dest and merge them into dvmap. + coalesceGlobals(printf, dvmap, dest, subPrefix, merge) + // Now coalesce the rest of the values. + var err error + dest[sub.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) + if err != nil { + return dest, err + } + } + } + return dest, nil +} + +// coalesceGlobals copies the globals out of src and merges them into dest. +// +// For convenience, returns dest. +func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) { + var dg, sg map[string]interface{} + + if destglob, ok := dest[common.GlobalKey]; !ok { + dg = make(map[string]interface{}) + } else if dg, ok = destglob.(map[string]interface{}); !ok { + printf("warning: skipping globals because destination %s is not a table.", common.GlobalKey) + return + } + + if srcglob, ok := src[common.GlobalKey]; !ok { + sg = make(map[string]interface{}) + } else if sg, ok = srcglob.(map[string]interface{}); !ok { + printf("warning: skipping globals because source %s is not a table.", common.GlobalKey) + return + } + + // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This + // reverses that decision. It may somehow be possible to introduce a loop + // here, but I haven't found a way. So for the time being, let's allow + // tables in globals. + for key, val := range sg { + if istable(val) { + vv := copyMap(val.(map[string]interface{})) + if destv, ok := dg[key]; !ok { + // Here there is no merge. We're just adding. + dg[key] = vv + } else { + if destvmap, ok := destv.(map[string]interface{}); !ok { + printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) + } else { + // Basically, we reverse order of coalesce here to merge + // top-down. + subPrefix := concatPrefix(prefix, key) + // In this location coalesceTablesFullKey should always have + // merge set to true. The output of coalesceGlobals is run + // through coalesce where any nils will be removed. + coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true) + dg[key] = vv + } + } + } else if dv, ok := dg[key]; ok && istable(dv) { + // It's not clear if this condition can actually ever trigger. + printf("key %s is table. Skipping", key) + } else { + // TODO: Do we need to do any additional checking on the value? + dg[key] = val + } + } + dest[common.GlobalKey] = dg +} + +func copyMap(src map[string]interface{}) map[string]interface{} { + m := make(map[string]interface{}, len(src)) + maps.Copy(m, src) + return m +} + +// coalesceValues builds up a values map for a particular chart. +// +// Values in v will override the values in the chart. +func coalesceValues(printf printFn, c chart.Charter, v map[string]interface{}, prefix string, merge bool) { + ch, err := chart.NewAccessor(c) + if err != nil { + return + } + + subPrefix := concatPrefix(prefix, ch.Name()) + + // Using c.Values directly when coalescing a table can cause problems where + // the original c.Values is altered. Creating a deep copy stops the problem. + // This section is fault-tolerant as there is no ability to return an error. + valuesCopy, err := copystructure.Copy(ch.Values()) + var vc map[string]interface{} + var ok bool + if err != nil { + // If there is an error something is wrong with copying c.Values it + // means there is a problem in the deep copying package or something + // wrong with c.Values. In this case we will use c.Values and report + // an error. + printf("warning: unable to copy values, err: %s", err) + vc = ch.Values() + } else { + vc, ok = valuesCopy.(map[string]interface{}) + if !ok { + // c.Values has a map[string]interface{} structure. If the copy of + // it cannot be treated as map[string]interface{} there is something + // strangely wrong. Log it and use c.Values + printf("warning: unable to convert values copy to values type") + vc = ch.Values() + } + } + + for key, val := range vc { + if value, ok := v[key]; ok { + if value == nil && !merge { + // When the YAML value is null and we are coalescing instead of + // merging, we remove the value's key. + // This allows Helm's various sources of values (value files or --set) to + // remove incompatible keys from any previous chart, file, or set values. + delete(v, key) + } else if dest, ok := value.(map[string]interface{}); ok { + // if v[key] is a table, merge nv's val table into v[key]. + src, ok := val.(map[string]interface{}) + if !ok { + // If the original value is nil, there is nothing to coalesce, so we don't print + // the warning + if val != nil { + printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key) + } + } else { + // If the key is a child chart, coalesce tables with Merge set to true + merge := childChartMergeTrue(c, key, merge) + + // Because v has higher precedence than nv, dest values override src + // values. + coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) + } + } + } else { + // If the key is not in v, copy it from nv. + v[key] = val + } + } +} + +func childChartMergeTrue(chrt chart.Charter, key string, merge bool) bool { + ch, err := chart.NewAccessor(chrt) + if err != nil { + return merge + } + for _, subchart := range ch.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return merge + } + if sub.Name() == key { + return true + } + } + return merge +} + +// CoalesceTables merges a source map into a destination map. +// +// dest is considered authoritative. +func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { + return coalesceTablesFullKey(log.Printf, dst, src, "", false) +} + +func MergeTables(dst, src map[string]interface{}) map[string]interface{} { + return coalesceTablesFullKey(log.Printf, dst, src, "", true) +} + +// coalesceTablesFullKey merges a source map into a destination map. +// +// dest is considered authoritative. +func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} { + // When --reuse-values is set but there are no modifications yet, return new values + if src == nil { + return dst + } + if dst == nil { + return src + } + // Track original non-nil src keys before modifying src + // This lets us distinguish between user nullifying a chart default vs + // user setting nil for a key not in chart defaults. + srcOriginalNonNil := make(map[string]bool) + for key, val := range src { + if val != nil { + srcOriginalNonNil[key] = true + } + } + for key, val := range dst { + if val == nil { + src[key] = nil + } + } + // Because dest has higher precedence than src, dest values override src + // values. + for key, val := range src { + fullkey := concatPrefix(prefix, key) + if dv, ok := dst[key]; ok && !merge && dv == nil && srcOriginalNonNil[key] { + // When coalescing (not merging), if dst has nil and src has a non-nil + // value, the user is nullifying a chart default - remove the key. + // But if src also has nil (or key not in src), preserve the nil + delete(dst, key) + } else if !ok { + // key not in user values, preserve src value (including nil) + dst[key] = val + } else if istable(val) { + if istable(dv) { + coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge) + } else { + printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) + } + } else if istable(dv) && val != nil { + printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val) + } + } + return dst +} + +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + +func makeValues(chrt chart.Charter, vals map[string]interface{}) (map[string]interface{}, error) { + var secretsRuntimeData common.RuntimeData + var extraValues map[string]interface{} + + switch c := chrt.(type) { + case *v2.Chart: + secretsRuntimeData = c.SecretsRuntimeData + extraValues = c.ExtraValues + case *v3.Chart: + secretsRuntimeData = c.SecretsRuntimeData + extraValues = c.ExtraValues + default: + return vals, nil + } + + var decryptedSecretValues map[string]interface{} + if secretsRuntimeData != nil { + decryptedSecretValues = secretsRuntimeData.GetDecryptedSecretValues() + } + + result, err := MergeInternal( + context.Background(), + vals, + extraValues, + decryptedSecretValues, + ) + if err != nil { + return vals, err + } + + return result, nil +} + +func MergeInternal(ctx context.Context, inputVals, serviceVals map[string]interface{}, decryptedSecretValues map[string]interface{}) (map[string]interface{}, error) { + vals := make(map[string]interface{}) + + CoalesceTables(vals, serviceVals) + + if decryptedSecretValues != nil { + CoalesceTables(vals, decryptedSecretValues) + } + + CoalesceTables(vals, inputVals) + + return vals, nil +} + +func init() { + nelmcommon.LegacyCoalesceTablesFunc = CoalesceTables +} diff --git a/pkg/helm/pkg/chartutil/coalesce_test.go b/pkg/helm/pkg/chart/common/util/coalesce_test.go similarity index 87% rename from pkg/helm/pkg/chartutil/coalesce_test.go rename to pkg/helm/pkg/chart/common/util/coalesce_test.go index 772ea036..24e0b376 100644 --- a/pkg/helm/pkg/chartutil/coalesce_test.go +++ b/pkg/helm/pkg/chart/common/util/coalesce_test.go @@ -14,16 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package chartutil +package util import ( + "bytes" "encoding/json" "fmt" + "maps" "testing" + "text/template" "github.com/stretchr/testify/assert" - "github.com/werf/nelm/pkg/helm/pkg/chart" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" ) // ref: http://www.yaml.org/spec/1.2/spec.html#id2803362 @@ -44,18 +48,21 @@ global: boat: true pequod: + boat: null global: name: Stinky harpooner: Tashtego nested: boat: false sail: true + foo2: null ahab: scope: whale boat: null nested: foo: true - bar: null + boat: null + object: null `) func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart { @@ -82,6 +89,13 @@ func TestCoalesceValues(t *testing.T) { "global": map[string]interface{}{ "nested2": map[string]interface{}{"l0": "moby"}, }, + "pequod": map[string]interface{}{ + "boat": "maybe", + "ahab": map[string]interface{}{ + "boat": "maybe", + "nested": map[string]interface{}{"boat": "maybe"}, + }, + }, }, }, withDeps(&chart.Chart{ @@ -92,19 +106,25 @@ func TestCoalesceValues(t *testing.T) { "global": map[string]interface{}{ "nested2": map[string]interface{}{"l1": "pequod"}, }, + "boat": false, + "ahab": map[string]interface{}{ + "boat": false, + "nested": map[string]interface{}{"boat": false}, + }, }, }, &chart.Chart{ Metadata: &chart.Metadata{Name: "ahab"}, Values: map[string]interface{}{ "global": map[string]interface{}{ - "nested": map[string]interface{}{"foo": "bar"}, + "nested": map[string]interface{}{"foo": "bar", "foo2": "bar2"}, "nested2": map[string]interface{}{"l2": "ahab"}, }, "scope": "ahab", "name": "ahab", "boat": true, - "nested": map[string]interface{}{"foo": false, "bar": true}, + "nested": map[string]interface{}{"foo": false, "boat": true}, + "object": map[string]interface{}{"foo": "bar"}, }, }, ), @@ -119,7 +139,7 @@ func TestCoalesceValues(t *testing.T) { }, ) - vals, err := ReadValues(testCoalesceValuesYaml) + vals, err := common.ReadValues(testCoalesceValuesYaml) if err != nil { t.Fatal(err) } @@ -127,10 +147,8 @@ func TestCoalesceValues(t *testing.T) { // taking a copy of the values before passing it // to CoalesceValues as argument, so that we can // use it for asserting later - valsCopy := make(Values, len(vals)) - for key, value := range vals { - valsCopy[key] = value - } + valsCopy := make(common.Values, len(vals)) + maps.Copy(valsCopy, vals) v, err := CoalesceValues(c, vals) if err != nil { @@ -155,6 +173,7 @@ func TestCoalesceValues(t *testing.T) { {"{{.pequod.ahab.nested.foo}}", "true"}, {"{{.pequod.ahab.global.name}}", "Ishmael"}, {"{{.pequod.ahab.global.nested.foo}}", "bar"}, + {"{{.pequod.ahab.global.nested.foo2}}", ""}, {"{{.pequod.ahab.global.subject}}", "Queequeg"}, {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, {"{{.pequod.global.name}}", "Ishmael"}, @@ -200,19 +219,35 @@ func TestCoalesceValues(t *testing.T) { t.Error("Expected nested boat key to be removed, still present") } - subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) + subchart := v["pequod"].(map[string]interface{}) if _, ok := subchart["boat"]; ok { t.Error("Expected subchart boat key to be removed, still present") } - if _, ok := subchart["nested"].(map[string]interface{})["bar"]; ok { - t.Error("Expected subchart nested bar key to be removed, still present") + subsubchart := subchart["ahab"].(map[string]interface{}) + if _, ok := subsubchart["boat"]; ok { + t.Error("Expected sub-subchart ahab boat key to be removed, still present") + } + + if _, ok := subsubchart["nested"].(map[string]interface{})["boat"]; ok { + t.Error("Expected sub-subchart nested boat key to be removed, still present") + } + + if _, ok := subsubchart["object"]; ok { + t.Error("Expected sub-subchart object map to be removed, still present") } // CoalesceValues should not mutate the passed arguments is.Equal(valsCopy, vals) } +func ttpl(tpl string, v map[string]interface{}) (string, error) { + var b bytes.Buffer + tt := template.Must(template.New("t").Parse(tpl)) + err := tt.Execute(&b, v) + return b.String(), err +} + func TestMergeValues(t *testing.T) { is := assert.New(t) @@ -269,7 +304,7 @@ func TestMergeValues(t *testing.T) { }, ) - vals, err := ReadValues(testCoalesceValuesYaml) + vals, err := common.ReadValues(testCoalesceValuesYaml) if err != nil { t.Fatal(err) } @@ -277,10 +312,8 @@ func TestMergeValues(t *testing.T) { // taking a copy of the values before passing it // to MergeValues as argument, so that we can // use it for asserting later - valsCopy := make(Values, len(vals)) - for key, value := range vals { - valsCopy[key] = value - } + valsCopy := make(common.Values, len(vals)) + maps.Copy(valsCopy, vals) v, err := MergeValues(c, vals) if err != nil { @@ -698,3 +731,37 @@ func TestConcatPrefix(t *testing.T) { assert.Equal(t, "b", concatPrefix("", "b")) assert.Equal(t, "a.b", concatPrefix("a", "b")) } + +// TestCoalesceValuesEmptyMapWithNils tests the full CoalesceValues scenario +// from issue #31643 where chart has data: {} and user provides data: {foo: bar, baz: ~} +func TestCoalesceValuesEmptyMapWithNils(t *testing.T) { + is := assert.New(t) + + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + Values: map[string]any{ + "data": map[string]any{}, // empty map in chart defaults + }, + } + + vals := map[string]any{ + "data": map[string]any{ + "foo": "bar", + "baz": nil, // explicit nil from user + }, + } + + v, err := CoalesceValues(c, vals) + is.NoError(err) + + data, ok := v["data"].(map[string]any) + is.True(ok, "data is not a map") + + // "foo" should be preserved + is.Equal("bar", data["foo"]) + + // "baz" should be preserved with nil value since it wasn't in chart defaults + _, ok = data["baz"] + is.True(ok, "Expected data.baz key to be present but it was removed") + is.Nil(data["baz"], "Expected data.baz key to be nil but it is not") +} diff --git a/pkg/helm/pkg/chart/common/util/jsonschema.go b/pkg/helm/pkg/chart/common/util/jsonschema.go new file mode 100644 index 00000000..25f50009 --- /dev/null +++ b/pkg/helm/pkg/chart/common/util/jsonschema.go @@ -0,0 +1,216 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "bytes" + "crypto/tls" + "errors" + "fmt" + "log/slog" + "net/http" + "strings" + "sync" + "time" + + "github.com/santhosh-tekuri/jsonschema/v6" + + "github.com/werf/nelm/pkg/helm/intern/version" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +// HTTPURLLoader implements a loader for HTTP/HTTPS URLs +type HTTPURLLoader http.Client + +func (l *HTTPURLLoader) Load(urlStr string) (any, error) { + client := (*http.Client)(l) + + req, err := http.NewRequest(http.MethodGet, urlStr, nil) + if err != nil { + return nil, fmt.Errorf("failed to create HTTP request for %s: %w", urlStr, err) + } + req.Header.Set("User-Agent", version.GetUserAgent()) + + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP request failed for %s: %w", urlStr, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP request to %s returned status %d (%s)", urlStr, resp.StatusCode, http.StatusText(resp.StatusCode)) + } + + return jsonschema.UnmarshalJSON(resp.Body) +} + +// newHTTPURLLoader creates a HTTP URL loader with proxy support. +func newHTTPURLLoader() *HTTPURLLoader { + httpLoader := HTTPURLLoader(http.Client{ + Timeout: 15 * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + TLSClientConfig: &tls.Config{}, + }, + }) + return &httpLoader +} + +// ValidateAgainstSchema checks that values does not violate the structure laid out in schema +func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) error { + chrt, err := chart.NewAccessor(ch) + if err != nil { + return err + } + var sb strings.Builder + if chrt.Schema() != nil { + slog.Debug("chart name", "chart-name", chrt.Name()) + err := ValidateAgainstSingleSchema(values, chrt.Schema()) + if err != nil { + fmt.Fprintf(&sb, "%s:\n", chrt.Name()) + sb.WriteString(err.Error()) + } + } + slog.Debug("number of dependencies in the chart", "chart", chrt.Name(), "dependencies", len(chrt.Dependencies())) + // For each dependency, recursively call this function with the coalesced values + for _, subchart := range chrt.Dependencies() { + sub, err := chart.NewAccessor(subchart) + if err != nil { + return err + } + + raw, exists := values[sub.Name()] + if !exists || raw == nil { + // No values provided for this subchart; nothing to validate + continue + } + + subchartValues, ok := raw.(map[string]any) + if !ok { + fmt.Fprintf(&sb, "%s:\ninvalid type for values: expected object (map), got %T\n", + sub.Name(), raw) + continue + } + + if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { + sb.WriteString(err.Error()) + } + } + + if sb.Len() > 0 { + return errors.New(sb.String()) + } + + return nil +} + +// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema +func ValidateAgainstSingleSchema(values common.Values, schemaJSON []byte) (reterr error) { + defer func() { + if r := recover(); r != nil { + reterr = fmt.Errorf("unable to validate schema: %s", r) + } + }() + + // This unmarshal function leverages UseNumber() for number precision. The parser + // used for values does this as well. + schema, err := jsonschema.UnmarshalJSON(bytes.NewReader(schemaJSON)) + if err != nil { + return err + } + slog.Debug("unmarshalled JSON schema", "schema", schemaJSON) + + // Configure compiler with loaders for different URL schemes + loader := jsonschema.SchemeURLLoader{ + "file": jsonschema.FileLoader{}, + "http": newHTTPURLLoader(), + "https": newHTTPURLLoader(), + "urn": urnLoader{}, + } + + compiler := jsonschema.NewCompiler() + compiler.UseLoader(loader) + err = compiler.AddResource("file:///values.schema.json", schema) + if err != nil { + return err + } + + validator, err := compiler.Compile("file:///values.schema.json") + if err != nil { + return err + } + + err = validator.Validate(values.AsMap()) + if err != nil { + return JSONSchemaValidationError{err} + } + + return nil +} + +// URNResolverFunc allows SDK to plug a URN resolver. It must return a +// schema document compatible with the validator (e.g., result of +// jsonschema.UnmarshalJSON). +type URNResolverFunc func(urn string) (any, error) + +// URNResolver is the default resolver used by the URN loader. By default it +// returns a clear error. +var URNResolver URNResolverFunc = func(urn string) (any, error) { + return nil, fmt.Errorf("URN not resolved: %s", urn) +} + +// urnLoader implements resolution for the urn: scheme by delegating to +// URNResolver. If unresolved, it logs a warning and returns a permissive +// boolean-true schema to avoid hard failures (back-compat behavior). +type urnLoader struct{} + +// warnedURNs ensures we log the unresolved-URN warning only once per URN. +var warnedURNs sync.Map + +func (l urnLoader) Load(urlStr string) (any, error) { + if doc, err := URNResolver(urlStr); err == nil && doc != nil { + return doc, nil + } + if _, loaded := warnedURNs.LoadOrStore(urlStr, struct{}{}); !loaded { + slog.Warn("unresolved URN reference ignored; using permissive schema", "urn", urlStr) + } + return jsonschema.UnmarshalJSON(strings.NewReader("true")) +} + +// Note, JSONSchemaValidationError is used to wrap the error from the underlying +// validation package so that Helm has a clean interface and the validation package +// could be replaced without changing the Helm SDK API. + +// JSONSchemaValidationError is the error returned when there is a schema validation +// error. +type JSONSchemaValidationError struct { + embeddedErr error +} + +// Error prints the error message +func (e JSONSchemaValidationError) Error() string { + errStr := e.embeddedErr.Error() + + // This string prefixes all of our error details. Further up the stack of helm error message + // building more detail is provided to users. This is removed. + errStr = strings.TrimPrefix(errStr, "jsonschema validation failed with 'file:///values.schema.json#'\n") + + // The extra new line is needed for when there are sub-charts. + return errStr + "\n" +} diff --git a/pkg/helm/pkg/chart/common/util/jsonschema_test.go b/pkg/helm/pkg/chart/common/util/jsonschema_test.go new file mode 100644 index 00000000..507c0a46 --- /dev/null +++ b/pkg/helm/pkg/chart/common/util/jsonschema_test.go @@ -0,0 +1,391 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func TestValidateAgainstSingleSchema(t *testing.T) { + values, err := common.ReadValuesFile("./testdata/test-values.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := os.ReadFile("./testdata/test-values.schema.json") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + + if err := ValidateAgainstSingleSchema(values, schema); err != nil { + t.Errorf("Error validating Values against Schema: %s", err) + } +} + +func TestValidateAgainstInvalidSingleSchema(t *testing.T) { + values, err := common.ReadValuesFile("./testdata/test-values.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + + var errString string + if err := ValidateAgainstSingleSchema(values, schema); err == nil { + t.Fatalf("Expected an error, but got nil") + } else { + errString = err.Error() + } + + expectedErrString := `"file:///values.schema.json#" is not valid against metaschema: jsonschema validation failed with 'https://json-schema.org/draft/2020-12/schema#' +- at '': got number, want boolean or object` + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} + +func TestValidateAgainstSingleSchemaNegative(t *testing.T) { + values, err := common.ReadValuesFile("./testdata/test-values-negative.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + schema, err := os.ReadFile("./testdata/test-values.schema.json") + if err != nil { + t.Fatalf("Error reading JSON file: %s", err) + } + + var errString string + if err := ValidateAgainstSingleSchema(values, schema); err == nil { + t.Fatalf("Expected an error, but got nil") + } else { + errString = err.Error() + } + + expectedErrString := `- at '': missing property 'employmentInfo' +- at '/age': minimum: got -5, want 0 +` + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} + +const subchartSchema = `{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Values", + "type": "object", + "properties": { + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "age" + ] +} +` + +const subchartSchema2020 = `{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Values", + "type": "object", + "properties": { + "data": { + "type": "array", + "contains": { "type": "string" }, + "unevaluatedItems": { "type": "number" } + } + }, + "required": ["data"] +} +` + +func TestValidateAgainstSchema(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "subchart", + }, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chrt", + }, + } + chrt.AddDependency(subchart) + + vals := map[string]interface{}{ + "name": "John", + "subchart": map[string]interface{}{ + "age": 25, + }, + } + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Errorf("Error validating Values against Schema: %s", err) + } +} + +func TestValidateAgainstSchemaNegative(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "subchart", + }, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chrt", + }, + } + chrt.AddDependency(subchart) + + vals := map[string]interface{}{ + "name": "John", + "subchart": map[string]interface{}{}, + } + + var errString string + if err := ValidateAgainstSchema(chrt, vals); err == nil { + t.Fatalf("Expected an error, but got nil") + } else { + errString = err.Error() + } + + expectedErrString := `subchart: +- at '': missing property 'age' +` + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} + +func TestValidateAgainstSchema2020(t *testing.T) { + subchartJSON := []byte(subchartSchema2020) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "subchart", + }, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chrt", + }, + } + chrt.AddDependency(subchart) + + vals := map[string]interface{}{ + "name": "John", + "subchart": map[string]interface{}{ + "data": []any{"hello", 12}, + }, + } + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Errorf("Error validating Values against Schema: %s", err) + } +} + +func TestValidateAgainstSchema2020Negative(t *testing.T) { + subchartJSON := []byte(subchartSchema2020) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "subchart", + }, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "chrt", + }, + } + chrt.AddDependency(subchart) + + vals := map[string]interface{}{ + "name": "John", + "subchart": map[string]interface{}{ + "data": []any{12}, + }, + } + + var errString string + if err := ValidateAgainstSchema(chrt, vals); err == nil { + t.Fatalf("Expected an error, but got nil") + } else { + errString = err.Error() + } + + expectedErrString := `subchart: +- at '/data': no items match contains schema + - at '/data/0': got number, want string +` + if errString != expectedErrString { + t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) + } +} + +func TestHTTPURLLoader_Load(t *testing.T) { + // Test successful JSON schema loading + t.Run("successful load", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"type": "object", "properties": {"name": {"type": "string"}}}`)) + })) + defer server.Close() + + loader := newHTTPURLLoader() + result, err := loader.Load(server.URL) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if result == nil { + t.Fatal("Expected result to be non-nil") + } + }) + + t.Run("HTTP error status", func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer server.Close() + + loader := newHTTPURLLoader() + _, err := loader.Load(server.URL) + if err == nil { + t.Fatal("Expected error for HTTP 404") + } + if !strings.Contains(err.Error(), "404") { + t.Errorf("Expected error message to contain '404', got: %v", err) + } + }) +} + +// Test that an unresolved URN $ref is soft-ignored and validation succeeds. +// it mimics the behavior of Helm 3.18.4 +func TestValidateAgainstSingleSchema_UnresolvedURN_Ignored(t *testing.T) { + schema := []byte(`{ + "$schema": "https://json-schema.org/draft-07/schema#", + "$ref": "urn:example:helm:schemas:v1:helm-schema-validation-conditions:v1/helmSchemaValidation-true" + }`) + vals := map[string]interface{}{"any": "value"} + if err := ValidateAgainstSingleSchema(vals, schema); err != nil { + t.Fatalf("expected no error when URN unresolved is ignored, got: %v", err) + } +} + +// Non-regression tests for https://github.com/helm/helm/issues/31202 +// Ensure ValidateAgainstSchema does not panic when: +// - subchart key is missing +// - subchart value is nil +// - subchart value has an invalid type + +func TestValidateAgainstSchema_MissingSubchartValues_NoPanic(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "subchart"}, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{Name: "chrt"}, + } + chrt.AddDependency(subchart) + + // No "subchart" key present in values + vals := map[string]any{ + "name": "John", + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("ValidateAgainstSchema panicked (missing subchart values): %v", r) + } + }() + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Fatalf("expected no error when subchart values are missing, got: %v", err) + } +} + +func TestValidateAgainstSchema_SubchartNil_NoPanic(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "subchart"}, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{Name: "chrt"}, + } + chrt.AddDependency(subchart) + + // "subchart" key present but nil + vals := map[string]any{ + "name": "John", + "subchart": nil, + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("ValidateAgainstSchema panicked (nil subchart values): %v", r) + } + }() + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Fatalf("expected no error when subchart values are nil, got: %v", err) + } +} + +func TestValidateAgainstSchema_InvalidSubchartValuesType_NoPanic(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "subchart"}, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{Name: "chrt"}, + } + chrt.AddDependency(subchart) + + // "subchart" is the wrong type (string instead of map) + vals := map[string]any{ + "name": "John", + "subchart": "oops", + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("ValidateAgainstSchema panicked (invalid subchart values type): %v", r) + } + }() + + // We expect a non-nil error (invalid type), but crucially no panic. + if err := ValidateAgainstSchema(chrt, vals); err == nil { + t.Fatalf("expected an error when subchart values have invalid type, got nil") + } +} diff --git a/pkg/helm/pkg/chart/common/util/testdata/test-values-invalid.schema.json b/pkg/helm/pkg/chart/common/util/testdata/test-values-invalid.schema.json new file mode 100644 index 00000000..35a16a2c --- /dev/null +++ b/pkg/helm/pkg/chart/common/util/testdata/test-values-invalid.schema.json @@ -0,0 +1 @@ + 1E1111111 diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.yaml b/pkg/helm/pkg/chart/common/util/testdata/test-values-negative.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.yaml rename to pkg/helm/pkg/chart/common/util/testdata/test-values-negative.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.schema.json b/pkg/helm/pkg/chart/common/util/testdata/test-values.schema.json similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/values.schema.json rename to pkg/helm/pkg/chart/common/util/testdata/test-values.schema.json diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/values.yaml b/pkg/helm/pkg/chart/common/util/testdata/test-values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/values.yaml rename to pkg/helm/pkg/chart/common/util/testdata/test-values.yaml diff --git a/pkg/helm/pkg/chart/common/util/values.go b/pkg/helm/pkg/chart/common/util/values.go new file mode 100644 index 00000000..509de42d --- /dev/null +++ b/pkg/helm/pkg/chart/common/util/values.go @@ -0,0 +1,70 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + + "github.com/werf/nelm/pkg/helm/pkg/chart" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files +// +// This takes both ReleaseOptions and Capabilities to merge into the render values. +func ToRenderValues(chrt chart.Charter, chrtVals map[string]interface{}, options common.ReleaseOptions, caps *common.Capabilities) (common.Values, error) { + return ToRenderValuesWithSchemaValidation(chrt, chrtVals, options, caps, false) +} + +// ToRenderValuesWithSchemaValidation composes the struct from the data coming from the Releases, Charts and Values files +// +// This takes both ReleaseOptions and Capabilities to merge into the render values. +func ToRenderValuesWithSchemaValidation(chrt chart.Charter, chrtVals map[string]interface{}, options common.ReleaseOptions, caps *common.Capabilities, skipSchemaValidation bool) (common.Values, error) { + if caps == nil { + caps = common.DefaultCapabilities + } + accessor, err := chart.NewAccessor(chrt) + if err != nil { + return nil, err + } + top := map[string]interface{}{ + "Chart": accessor.MetadataAsMap(), + "Capabilities": caps, + "Release": map[string]interface{}{ + "Name": options.Name, + "Namespace": options.Namespace, + "IsUpgrade": options.IsUpgrade, + "IsInstall": options.IsInstall, + "Revision": options.Revision, + "Service": "Helm", + }, + } + + vals, err := CoalesceValues(chrt, chrtVals) + if err != nil { + return common.Values(top), err + } + + if !skipSchemaValidation { + if err := ValidateAgainstSchema(chrt, vals); err != nil { + return top, fmt.Errorf("values don't meet the specifications of the schema(s) in the following chart(s):\n%w", err) + } + } + + top["Values"] = vals + return top, nil +} diff --git a/pkg/helm/pkg/chart/common/util/values_test.go b/pkg/helm/pkg/chart/common/util/values_test.go new file mode 100644 index 00000000..b983319a --- /dev/null +++ b/pkg/helm/pkg/chart/common/util/values_test.go @@ -0,0 +1,112 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func TestToRenderValues(t *testing.T) { + + chartValues := map[string]interface{}{ + "name": "al Rashid", + "where": map[string]interface{}{ + "city": "Basrah", + "title": "caliph", + }, + } + + overrideValues := map[string]interface{}{ + "name": "Haroun", + "where": map[string]interface{}{ + "city": "Baghdad", + "date": "809 CE", + }, + } + + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "test"}, + Templates: []*common.File{}, + Values: chartValues, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, + }, + } + c.AddDependency(&chart.Chart{ + Metadata: &chart.Metadata{Name: "where"}, + }) + + o := common.ReleaseOptions{ + Name: "Seven Voyages", + Namespace: "default", + Revision: 1, + IsInstall: true, + } + + res, err := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) + if err != nil { + t.Fatal(err) + } + + // Ensure that the top-level values are all set. + metamap := res["Chart"].(map[string]interface{}) + if name := metamap["Name"]; name.(string) != "test" { + t.Errorf("Expected chart name 'test', got %q", name) + } + relmap := res["Release"].(map[string]interface{}) + if name := relmap["Name"]; name.(string) != "Seven Voyages" { + t.Errorf("Expected release name 'Seven Voyages', got %q", name) + } + if namespace := relmap["Namespace"]; namespace.(string) != "default" { + t.Errorf("Expected namespace 'default', got %q", namespace) + } + if revision := relmap["Revision"]; revision.(int) != 1 { + t.Errorf("Expected revision '1', got %d", revision) + } + if relmap["IsUpgrade"].(bool) { + t.Error("Expected upgrade to be false.") + } + if !relmap["IsInstall"].(bool) { + t.Errorf("Expected install to be true.") + } + if !res["Capabilities"].(*common.Capabilities).APIVersions.Has("v1") { + t.Error("Expected Capabilities to have v1 as an API") + } + if res["Capabilities"].(*common.Capabilities).KubeVersion.Major != "1" { + t.Error("Expected Capabilities to have a Kube version") + } + + vals := res["Values"].(common.Values) + if vals["name"] != "Haroun" { + t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) + } + where := vals["where"].(map[string]interface{}) + expects := map[string]string{ + "city": "Baghdad", + "date": "809 CE", + "title": "caliph", + } + for field, expect := range expects { + if got := where[field]; got != expect { + t.Errorf("Expected %q, got %q (%v)", expect, got, where) + } + } +} diff --git a/pkg/helm/pkg/chart/common/values.go b/pkg/helm/pkg/chart/common/values.go new file mode 100644 index 00000000..94958a77 --- /dev/null +++ b/pkg/helm/pkg/chart/common/values.go @@ -0,0 +1,175 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "errors" + "io" + "os" + "strings" + + "sigs.k8s.io/yaml" +) + +// GlobalKey is the name of the Values key that is used for storing global vars. +const GlobalKey = "global" + +// Values represents a collection of chart values. +type Values map[string]interface{} + +// YAML encodes the Values into a YAML string. +func (v Values) YAML() (string, error) { + b, err := yaml.Marshal(v) + return string(b), err +} + +// Table gets a table (YAML subsection) from a Values object. +// +// The table is returned as a Values. +// +// Compound table names may be specified with dots: +// +// foo.bar +// +// The above will be evaluated as "The table bar inside the table +// foo". +// +// An ErrNoTable is returned if the table does not exist. +func (v Values) Table(name string) (Values, error) { + table := v + var err error + + for _, n := range parsePath(name) { + if table, err = tableLookup(table, n); err != nil { + break + } + } + return table, err +} + +// AsMap is a utility function for converting Values to a map[string]interface{}. +// +// It protects against nil map panics. +func (v Values) AsMap() map[string]interface{} { + if len(v) == 0 { + return map[string]interface{}{} + } + return v +} + +// Encode writes serialized Values information to the given io.Writer. +func (v Values) Encode(w io.Writer) error { + out, err := yaml.Marshal(v) + if err != nil { + return err + } + _, err = w.Write(out) + return err +} + +func tableLookup(v Values, simple string) (Values, error) { + v2, ok := v[simple] + if !ok { + return v, ErrNoTable{simple} + } + if vv, ok := v2.(map[string]interface{}); ok { + return vv, nil + } + + // This catches a case where a value is of type Values, but doesn't (for some + // reason) match the map[string]interface{}. This has been observed in the + // wild, and might be a result of a nil map of type Values. + if vv, ok := v2.(Values); ok { + return vv, nil + } + + return Values{}, ErrNoTable{simple} +} + +// ReadValues will parse YAML byte data into a Values. +func ReadValues(data []byte) (vals Values, err error) { + err = yaml.Unmarshal(data, &vals) + if len(vals) == 0 { + vals = Values{} + } + return vals, err +} + +// ReadValuesFile will parse a YAML file into a map of values. +func ReadValuesFile(filename string) (Values, error) { + data, err := os.ReadFile(filename) + if err != nil { + return map[string]interface{}{}, err + } + return ReadValues(data) +} + +// ReleaseOptions represents the additional release options needed +// for the composition of the final values struct +type ReleaseOptions struct { + Name string + Namespace string + Revision int + IsUpgrade bool + IsInstall bool +} + +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + +// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path. +// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods. +// Given the following YAML data the value at path "chapter.one.title" is "Loomings". +// +// chapter: +// one: +// title: "Loomings" +func (v Values) PathValue(path string) (interface{}, error) { + if path == "" { + return nil, errors.New("YAML path cannot be empty") + } + return v.pathValue(parsePath(path)) +} + +func (v Values) pathValue(path []string) (interface{}, error) { + if len(path) == 1 { + // if exists must be root key not table + if _, ok := v[path[0]]; ok && !istable(v[path[0]]) { + return v[path[0]], nil + } + return nil, ErrNoValue{path[0]} + } + + key, path := path[len(path)-1], path[:len(path)-1] + // get our table for table path + t, err := v.Table(joinPath(path...)) + if err != nil { + return nil, ErrNoValue{key} + } + // check table for key and ensure value is not a table + if k, ok := t[key]; ok && !istable(k) { + return k, nil + } + return nil, ErrNoValue{key} +} + +func parsePath(key string) []string { return strings.Split(key, ".") } + +func joinPath(path ...string) string { return strings.Join(path, ".") } diff --git a/pkg/helm/pkg/chart/common/values_test.go b/pkg/helm/pkg/chart/common/values_test.go new file mode 100644 index 00000000..3cceeb2b --- /dev/null +++ b/pkg/helm/pkg/chart/common/values_test.go @@ -0,0 +1,205 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "bytes" + "fmt" + "testing" + "text/template" +) + +func TestReadValues(t *testing.T) { + doc := `# Test YAML parse +poet: "Coleridge" +title: "Rime of the Ancient Mariner" +stanza: + - "at" + - "length" + - "did" + - cross + - an + - Albatross + +mariner: + with: "crossbow" + shot: "ALBATROSS" + +water: + water: + where: "everywhere" + nor: "any drop to drink" +` + + data, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Error parsing bytes: %s", err) + } + matchValues(t, data) + + tests := []string{`poet: "Coleridge"`, "# Just a comment", ""} + + for _, tt := range tests { + data, err = ReadValues([]byte(tt)) + if err != nil { + t.Fatalf("Error parsing bytes (%s): %s", tt, err) + } + if data == nil { + t.Errorf(`YAML string "%s" gave a nil map`, tt) + } + } +} + +func TestReadValuesFile(t *testing.T) { + data, err := ReadValuesFile("./testdata/coleridge.yaml") + if err != nil { + t.Fatalf("Error reading YAML file: %s", err) + } + matchValues(t, data) +} + +func ExampleValues() { + doc := ` +title: "Moby Dick" +chapter: + one: + title: "Loomings" + two: + title: "The Carpet-Bag" + three: + title: "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + panic(err) + } + ch1, err := d.Table("chapter.one") + if err != nil { + panic("could not find chapter one") + } + fmt.Print(ch1["title"]) + // Output: + // Loomings +} + +func TestTable(t *testing.T) { + doc := ` +title: "Moby Dick" +chapter: + one: + title: "Loomings" + two: + title: "The Carpet-Bag" + three: + title: "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Failed to parse the White Whale: %s", err) + } + + if _, err := d.Table("title"); err == nil { + t.Fatalf("Title is not a table.") + } + + if _, err := d.Table("chapter"); err != nil { + t.Fatalf("Failed to get the chapter table: %s\n%v", err, d) + } + + if v, err := d.Table("chapter.one"); err != nil { + t.Errorf("Failed to get chapter.one: %s", err) + } else if v["title"] != "Loomings" { + t.Errorf("Unexpected title: %s", v["title"]) + } + + if _, err := d.Table("chapter.three"); err != nil { + t.Errorf("Chapter three is missing: %s\n%v", err, d) + } + + if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil { + t.Errorf("I think you mean 'Epilogue'") + } +} + +func matchValues(t *testing.T, data map[string]interface{}) { + t.Helper() + if data["poet"] != "Coleridge" { + t.Errorf("Unexpected poet: %s", data["poet"]) + } + + if o, err := ttpl("{{len .stanza}}", data); err != nil { + t.Errorf("len stanza: %s", err) + } else if o != "6" { + t.Errorf("Expected 6, got %s", o) + } + + if o, err := ttpl("{{.mariner.shot}}", data); err != nil { + t.Errorf(".mariner.shot: %s", err) + } else if o != "ALBATROSS" { + t.Errorf("Expected that mariner shot ALBATROSS") + } + + if o, err := ttpl("{{.water.water.where}}", data); err != nil { + t.Errorf(".water.water.where: %s", err) + } else if o != "everywhere" { + t.Errorf("Expected water water everywhere") + } +} + +func ttpl(tpl string, v map[string]interface{}) (string, error) { + var b bytes.Buffer + tt := template.Must(template.New("t").Parse(tpl)) + err := tt.Execute(&b, v) + return b.String(), err +} + +func TestPathValue(t *testing.T) { + doc := ` +title: "Moby Dick" +chapter: + one: + title: "Loomings" + two: + title: "The Carpet-Bag" + three: + title: "The Spouter Inn" +` + d, err := ReadValues([]byte(doc)) + if err != nil { + t.Fatalf("Failed to parse the White Whale: %s", err) + } + + if v, err := d.PathValue("chapter.one.title"); err != nil { + t.Errorf("Got error instead of title: %s\n%v", err, d) + } else if v != "Loomings" { + t.Errorf("No error but got wrong value for title: %s\n%v", err, d) + } + if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil { + t.Errorf("Non-existent key should return error: %s\n%v", err, d) + } + if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil { + t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d) + } + if _, err := d.PathValue(""); err == nil { + t.Error("Asking for the value from an empty path should yield an error") + } + if v, err := d.PathValue("title"); err == nil { + if v != "Moby Dick" { + t.Errorf("Failed to return values for root key title") + } + } +} diff --git a/pkg/helm/pkg/chart/dependency.go b/pkg/helm/pkg/chart/dependency.go index 001c69ac..1226cc59 100644 --- a/pkg/helm/pkg/chart/dependency.go +++ b/pkg/helm/pkg/chart/dependency.go @@ -15,72 +15,50 @@ limitations under the License. package chart -import "time" +import ( + "errors" -// Dependency describes a chart upon which another chart depends. -// -// Dependencies can be used to express developer intent, or to capture the state -// of a chart. -type Dependency struct { - // Name is the name of the dependency. - // - // This must mach the name in the dependency's Chart.yaml. - Name string `json:"name"` - // Version is the version (range) of this chart. - // - // A lock file will always produce a single version, while a dependency - // may contain a semantic version range. - Version string `json:"version,omitempty"` - // The URL to the repository. - // - // Appending `index.yaml` to this string should result in a URL that can be - // used to fetch the repository index. - Repository string `json:"repository"` - // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) - Condition string `json:"condition,omitempty"` - // Tags can be used to group charts for enabling/disabling together - Tags []string `json:"tags,omitempty"` - // Enabled bool determines if chart should be loaded - Enabled bool `json:"enabled,omitempty"` - // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a - // string or pair of child/parent sublist items. - ImportValues []interface{} `json:"import-values,omitempty"` - // Alias usable alias to be used for the chart - Alias string `json:"alias,omitempty"` + v3chart "github.com/werf/nelm/pkg/helm/intern/chart/v3" + v2chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) - // ExportValues holds the mapping of parent values to child key to be exported. Each item can be a - // string or pair of parent/child sublist items. - ExportValues []interface{} `json:"export-values,omitempty"` -} +var NewDependencyAccessor func(dep Dependency) (DependencyAccessor, error) = NewDefaultDependencyAccessor //nolint:revive -// Validate checks for common problems with the dependency datastructure in -// the chart. This check must be done at load time before the dependency's charts are -// loaded. -func (d *Dependency) Validate() error { - if d == nil { - return ValidationError("dependencies must not contain empty or null nodes") - } - d.Name = sanitizeString(d.Name) - d.Version = sanitizeString(d.Version) - d.Repository = sanitizeString(d.Repository) - d.Condition = sanitizeString(d.Condition) - for i := range d.Tags { - d.Tags[i] = sanitizeString(d.Tags[i]) +func NewDefaultDependencyAccessor(dep Dependency) (DependencyAccessor, error) { + switch v := dep.(type) { + case v2chart.Dependency: + return &v2DependencyAccessor{&v}, nil + case *v2chart.Dependency: + return &v2DependencyAccessor{v}, nil + case v3chart.Dependency: + return &v3DependencyAccessor{&v}, nil + case *v3chart.Dependency: + return &v3DependencyAccessor{v}, nil + default: + return nil, errors.New("unsupported chart dependency type") } - if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) { - return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name) - } - return nil } -// Lock is a lock file for dependencies. -// -// It represents the state that the dependencies should be in. -type Lock struct { - // Generated is the date the lock file was last generated. - Generated time.Time `json:"generated"` - // Digest is a hash of the dependencies in Chart.yaml. - Digest string `json:"digest"` - // Dependencies is the list of dependencies that this lock file has locked. - Dependencies []*Dependency `json:"dependencies"` +type v2DependencyAccessor struct { + dep *v2chart.Dependency +} + +func (r *v2DependencyAccessor) Name() string { + return r.dep.Name +} + +func (r *v2DependencyAccessor) Alias() string { + return r.dep.Alias +} + +type v3DependencyAccessor struct { + dep *v3chart.Dependency +} + +func (r *v3DependencyAccessor) Name() string { + return r.dep.Name +} + +func (r *v3DependencyAccessor) Alias() string { + return r.dep.Alias } diff --git a/pkg/helm/pkg/chart/dependency_test.go b/pkg/helm/pkg/chart/dependency_test.go deleted file mode 100644 index 90488a96..00000000 --- a/pkg/helm/pkg/chart/dependency_test.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package chart - -import ( - "testing" -) - -func TestValidateDependency(t *testing.T) { - dep := &Dependency{ - Name: "example", - } - for value, shouldFail := range map[string]bool{ - "abcdefghijklmenopQRSTUVWXYZ-0123456780_": false, - "-okay": false, - "_okay": false, - "- bad": true, - " bad": true, - "bad\nvalue": true, - "bad ": true, - "bad$": true, - } { - dep.Alias = value - res := dep.Validate() - if res != nil && !shouldFail { - t.Errorf("Failed on case %q", dep.Alias) - } else if res == nil && shouldFail { - t.Errorf("Expected failure for %q", dep.Alias) - } - } -} diff --git a/pkg/helm/pkg/chart/errors.go b/pkg/helm/pkg/chart/errors.go deleted file mode 100644 index 2fad5f37..00000000 --- a/pkg/helm/pkg/chart/errors.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chart - -import "fmt" - -// ValidationError represents a data validation error. -type ValidationError string - -func (v ValidationError) Error() string { - return "validation: " + string(v) -} - -// ValidationErrorf takes a message and formatting options and creates a ValidationError -func ValidationErrorf(msg string, args ...interface{}) ValidationError { - return ValidationError(fmt.Sprintf(msg, args...)) -} diff --git a/pkg/helm/pkg/chart/file.go b/pkg/helm/pkg/chart/file.go deleted file mode 100644 index 9dd7c08d..00000000 --- a/pkg/helm/pkg/chart/file.go +++ /dev/null @@ -1,27 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chart - -// File represents a file as a name/value pair. -// -// By convention, name is a relative path within the scope of the chart's -// base directory. -type File struct { - // Name is the path-like name of the template. - Name string `json:"name"` - // Data is the template as byte data. - Data []byte `json:"data"` -} diff --git a/pkg/helm/pkg/chart/interfaces.go b/pkg/helm/pkg/chart/interfaces.go new file mode 100644 index 00000000..508dfb54 --- /dev/null +++ b/pkg/helm/pkg/chart/interfaces.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chart + +import ( + common "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +type Charter interface{} + +type Dependency interface{} + +type Accessor interface { + Name() string + IsRoot() bool + MetadataAsMap() map[string]interface{} + Files() []*common.File + Templates() []*common.File + ChartFullPath() string + IsLibraryChart() bool + Dependencies() []Charter + MetaDependencies() []Dependency + Values() map[string]interface{} + Schema() []byte + Deprecated() bool +} + +type DependencyAccessor interface { + Name() string + Alias() string +} diff --git a/pkg/helm/pkg/chart/loader/archive.go b/pkg/helm/pkg/chart/loader/archive.go deleted file mode 100644 index 6139f6c3..00000000 --- a/pkg/helm/pkg/chart/loader/archive.go +++ /dev/null @@ -1,206 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package loader - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "fmt" - "io" - "net/http" - "os" - "path" - "regexp" - "strings" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) - -// FileLoader loads a chart from a file -type FileLoader string - -// Load loads a chart -func (l FileLoader) Load(opts helmopts.HelmOptions) (*chart.Chart, error) { - return LoadFile(string(l), opts) -} - -// LoadFile loads from an archive file. -func LoadFile(name string, opts helmopts.HelmOptions) (*chart.Chart, error) { - if fi, err := os.Stat(name); err != nil { - return nil, err - } else if fi.IsDir() { - return nil, errors.New("cannot load a directory") - } - - raw, err := os.Open(name) - if err != nil { - return nil, err - } - defer raw.Close() - - err = ensureArchive(name, raw) - if err != nil { - return nil, err - } - - c, err := LoadArchive(raw, opts) - if err != nil { - if err == gzip.ErrHeader { - return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %s)", name, err) - } - } - return c, err -} - -// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive. -// -// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence -// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error -// if we didn't check for this. -func ensureArchive(name string, raw *os.File) error { - defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed. - - // Check the file format to give us a chance to provide the user with more actionable feedback. - buffer := make([]byte, 512) - _, err := raw.Read(buffer) - if err != nil && err != io.EOF { - return fmt.Errorf("file '%s' cannot be read: %s", name, err) - } - - // Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject. - // Fix for: https://github.com/helm/helm/issues/12261 - if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) { - // TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide - // variety of content (Makefile, .zshrc) as valid YAML without errors. - - // Wrong content type. Let's check if it's yaml and give an extra hint? - if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") { - return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name) - } - return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType) - } - return nil -} - -// isGZipApplication checks whether the achieve is of the application/x-gzip type. -func isGZipApplication(data []byte) bool { - sig := []byte("\x1F\x8B\x08") - return bytes.HasPrefix(data, sig) -} - -// LoadArchiveFiles reads in files out of an archive into memory. This function -// performs important path security checks and should always be used before -// expanding a tarball -func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { - unzipped, err := gzip.NewReader(in) - if err != nil { - return nil, err - } - defer unzipped.Close() - - files := []*BufferedFile{} - tr := tar.NewReader(unzipped) - for { - b := bytes.NewBuffer(nil) - hd, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - return nil, err - } - - if hd.FileInfo().IsDir() { - // Use this instead of hd.Typeflag because we don't have to do any - // inference chasing. - continue - } - - switch hd.Typeflag { - // We don't want to process these extension header files. - case tar.TypeXGlobalHeader, tar.TypeXHeader: - continue - } - - // Archive could contain \ if generated on Windows - delimiter := "/" - if strings.ContainsRune(hd.Name, '\\') { - delimiter = "\\" - } - - parts := strings.Split(hd.Name, delimiter) - n := strings.Join(parts[1:], delimiter) - - // Normalize the path to the / delimiter - n = strings.ReplaceAll(n, delimiter, "/") - - if path.IsAbs(n) { - return nil, errors.New("chart illegally contains absolute paths") - } - - n = path.Clean(n) - if n == "." { - // In this case, the original path was relative when it should have been absolute. - return nil, errors.Errorf("chart illegally contains content outside the base directory: %q", hd.Name) - } - if strings.HasPrefix(n, "..") { - return nil, errors.New("chart illegally references parent directory") - } - - // In some particularly arcane acts of path creativity, it is possible to intermix - // UNIX and Windows style paths in such a way that you produce a result of the form - // c:/foo even after all the built-in absolute path checks. So we explicitly check - // for this condition. - if drivePathPattern.MatchString(n) { - return nil, errors.New("chart contains illegally named files") - } - - if parts[0] == "Chart.yaml" { - return nil, errors.New("chart yaml not in base directory") - } - - if _, err := io.Copy(b, tr); err != nil { - return nil, err - } - - data := bytes.TrimPrefix(b.Bytes(), utf8bom) - - files = append(files, &BufferedFile{Name: n, Data: data}) - b.Reset() - } - - if len(files) == 0 { - return nil, errors.New("no files in chart archive") - } - return files, nil -} - -// LoadArchive loads from a reader containing a compressed tar archive. -func LoadArchive(in io.Reader, opts helmopts.HelmOptions) (*chart.Chart, error) { - files, err := LoadArchiveFiles(in) - if err != nil { - return nil, err - } - - return LoadFiles(files, opts) -} diff --git a/pkg/helm/pkg/chart/loader/archive/archive.go b/pkg/helm/pkg/chart/loader/archive/archive.go new file mode 100644 index 00000000..e98f5c33 --- /dev/null +++ b/pkg/helm/pkg/chart/loader/archive/archive.go @@ -0,0 +1,197 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// archive provides utility functions for working with Helm chart archive files +package archive + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + "time" +) + +// MaxDecompressedChartSize is the maximum size of a chart archive that will be +// decompressed. This is the decompressed size of all the files. +// The default value is 100 MiB. +var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB + +// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load. +// The size of the file is the decompressed version of it when it is stored in an archive. +var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB + +var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// BufferedFile represents an archive file buffered for later processing. +type BufferedFile struct { + Name string + ModTime time.Time + Data []byte +} + +// LoadArchiveFiles reads in files out of an archive into memory. This function +// performs important path security checks and should always be used before +// expanding a tarball +func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { + unzipped, err := gzip.NewReader(in) + if err != nil { + return nil, err + } + defer unzipped.Close() + + files := []*BufferedFile{} + tr := tar.NewReader(unzipped) + remainingSize := MaxDecompressedChartSize + for { + b := bytes.NewBuffer(nil) + hd, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + + if hd.FileInfo().IsDir() { + // Use this instead of hd.Typeflag because we don't have to do any + // inference chasing. + continue + } + + switch hd.Typeflag { + // We don't want to process these extension header files. + case tar.TypeXGlobalHeader, tar.TypeXHeader: + continue + } + + // Archive could contain \ if generated on Windows + delimiter := "/" + if strings.ContainsRune(hd.Name, '\\') { + delimiter = "\\" + } + + parts := strings.Split(hd.Name, delimiter) + n := strings.Join(parts[1:], delimiter) + + // Normalize the path to the / delimiter + n = strings.ReplaceAll(n, delimiter, "/") + + if path.IsAbs(n) { + return nil, errors.New("chart illegally contains absolute paths") + } + + n = path.Clean(n) + if n == "." { + // In this case, the original path was relative when it should have been absolute. + return nil, fmt.Errorf("chart illegally contains content outside the base directory: %q", hd.Name) + } + if strings.HasPrefix(n, "..") { + return nil, errors.New("chart illegally references parent directory") + } + + // In some particularly arcane acts of path creativity, it is possible to intermix + // UNIX and Windows style paths in such a way that you produce a result of the form + // c:/foo even after all the built-in absolute path checks. So we explicitly check + // for this condition. + if drivePathPattern.MatchString(n) { + return nil, errors.New("chart contains illegally named files") + } + + if parts[0] == "Chart.yaml" { + return nil, errors.New("chart yaml not in base directory") + } + + if hd.Size > remainingSize { + return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize) + } + + if hd.Size > MaxDecompressedFileSize { + return nil, fmt.Errorf("decompressed chart file %q is larger than the maximum file size %d", hd.Name, MaxDecompressedFileSize) + } + + limitedReader := io.LimitReader(tr, remainingSize) + + bytesWritten, err := io.Copy(b, limitedReader) + if err != nil { + return nil, err + } + + remainingSize -= bytesWritten + // When the bytesWritten are less than the file size it means the limit reader ended + // copying early. Here we report that error. This is important if the last file extracted + // is the one that goes over the limit. It assumes the Size stored in the tar header + // is correct, something many applications do. + if bytesWritten < hd.Size || remainingSize <= 0 { + return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize) + } + + data := bytes.TrimPrefix(b.Bytes(), utf8bom) + + files = append(files, &BufferedFile{Name: n, ModTime: hd.ModTime, Data: data}) + b.Reset() + } + + if len(files) == 0 { + return nil, errors.New("no files in chart archive") + } + return files, nil +} + +// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive. +// +// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence +// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error +// if we didn't check for this. +func EnsureArchive(name string, raw *os.File) error { + defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed. + + // Check the file format to give us a chance to provide the user with more actionable feedback. + buffer := make([]byte, 512) + _, err := raw.Read(buffer) + if err != nil && err != io.EOF { + return fmt.Errorf("file '%s' cannot be read: %s", name, err) + } + + // Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject. + // Fix for: https://github.com/helm/helm/issues/12261 + if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) { + // TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide + // variety of content (Makefile, .zshrc) as valid YAML without errors. + + // Wrong content type. Let's check if it's yaml and give an extra hint? + if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") { + return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name) + } + return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType) + } + return nil +} + +// isGZipApplication checks whether the archive is of the application/x-gzip type. +func isGZipApplication(data []byte) bool { + sig := []byte("\x1F\x8B\x08") + return bytes.HasPrefix(data, sig) +} diff --git a/pkg/helm/pkg/chart/loader/archive_test.go b/pkg/helm/pkg/chart/loader/archive/archive_test.go similarity index 93% rename from pkg/helm/pkg/chart/loader/archive_test.go rename to pkg/helm/pkg/chart/loader/archive/archive_test.go index 41b0af1a..2fe09e9b 100644 --- a/pkg/helm/pkg/chart/loader/archive_test.go +++ b/pkg/helm/pkg/chart/loader/archive/archive_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package loader +package archive import ( "archive/tar" @@ -31,8 +31,9 @@ func TestLoadArchiveFiles(t *testing.T) { }{ { name: "empty input should return no files", - generate: func(w *tar.Writer) {}, - check: func(t *testing.T, files []*BufferedFile, err error) { + generate: func(_ *tar.Writer) {}, + check: func(t *testing.T, _ []*BufferedFile, err error) { + t.Helper() if err.Error() != "no files in chart archive" { t.Fatalf(`expected "no files in chart archive", got [%#v]`, err) } @@ -61,6 +62,7 @@ func TestLoadArchiveFiles(t *testing.T) { } }, check: func(t *testing.T, files []*BufferedFile, err error) { + t.Helper() if err != nil { t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err) } diff --git a/pkg/helm/pkg/chart/loader/directory.go b/pkg/helm/pkg/chart/loader/directory.go deleted file mode 100644 index e4c94b7c..00000000 --- a/pkg/helm/pkg/chart/loader/directory.go +++ /dev/null @@ -1,215 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package loader - -import ( - "bytes" - "context" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/intern/sympath" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/ignore" - "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -var utf8bom = []byte{0xEF, 0xBB, 0xBF} - -// DirLoader loads a chart from a directory -type DirLoader string - -// Load loads the chart -func (l DirLoader) Load(opts helmopts.HelmOptions) (*chart.Chart, error) { - return LoadDir(string(l), opts) -} - -// LoadDir loads from a directory. -// -// This loads charts only from directories. -func LoadDir(dir string, opts helmopts.HelmOptions) (*chart.Chart, error) { - ctx := context.Background() - - var files []*BufferedFile - switch opts.ChartLoadOpts.ChartType { - case helmopts.ChartTypeChart: - var chartTreeFiles []*file.ChartExtenderBufferedFile - if file.ChartFileReader != nil { - chartFiles, err := file.ChartFileReader.LoadChartDir(ctx, dir) - if err != nil { - return nil, fmt.Errorf("load chart dir: %w", err) - } - - chartTreeFiles, err = LoadChartDependencies( - ctx, - file.ChartFileReader.LoadChartDir, - dir, - chartFiles, - opts, - ) - if err != nil { - return nil, fmt.Errorf("load chart dependencies: %w", err) - } - } else { - var chartFiles []*file.ChartExtenderBufferedFile - if files, err := GetFilesFromLocalFilesystem(dir); err != nil { - return nil, fmt.Errorf("load files from filesystem: %w", err) - } else { - chartFiles = convertBufferedFilesForChartExtender(files) - } - - var err error - chartTreeFiles, err = LoadChartDependencies( - ctx, - func(ctx context.Context, dir string) ([]*file.ChartExtenderBufferedFile, error) { - files, err := GetFilesFromLocalFilesystem(dir) - if err != nil { - return nil, fmt.Errorf("load files from filesystem: %w", err) - } - - return convertBufferedFilesForChartExtender(files), nil - }, - dir, - chartFiles, - opts, - ) - if err != nil { - return nil, fmt.Errorf("load chart dependencies: %w", err) - } - } - - files = convertChartExtenderFilesToBufferedFiles(chartTreeFiles) - case helmopts.ChartTypeSubchart: - var err error - files, err = GetFilesFromLocalFilesystem(dir) - if err != nil { - return &chart.Chart{}, err - } - case helmopts.ChartTypeChartStub: - var err error - files, err = GetFilesFromLocalFilesystem(dir) - if err != nil { - return &chart.Chart{}, err - } - case helmopts.ChartTypeBundle: - chartFiles, err := GetFilesFromLocalFilesystem(dir) - if err != nil { - return nil, fmt.Errorf("load files from filesystem: %w", err) - } - - chartTreeFiles, err := LoadChartDependencies( - ctx, - func(ctx context.Context, dir string) ([]*file.ChartExtenderBufferedFile, error) { - files, err := GetFilesFromLocalFilesystem(dir) - if err != nil { - return nil, fmt.Errorf("load files from filesystem: %w", err) - } - - return convertBufferedFilesForChartExtender(files), nil - }, - dir, - convertBufferedFilesForChartExtender(chartFiles), - opts, - ) - if err != nil { - return nil, fmt.Errorf("load chart dependencies: %w", err) - } - - files = convertChartExtenderFilesToBufferedFiles(chartTreeFiles) - default: - panic("unexpected type") - } - - return LoadFiles(files, opts) -} - -func GetFilesFromLocalFilesystem(dir string) ([]*BufferedFile, error) { - topdir, err := filepath.Abs(dir) - if err != nil { - return nil, err - } - - rules := ignore.Empty() - ifile := filepath.Join(topdir, ignore.HelmIgnore) - if _, err := os.Stat(ifile); err == nil { - r, err := ignore.ParseFile(ifile) - if err != nil { - return nil, err - } - rules = r - } - rules.AddDefaults() - - files := []*BufferedFile{} - topdir += string(filepath.Separator) - - walk := func(name string, fi os.FileInfo, err error) error { - n := strings.TrimPrefix(name, topdir) - if n == "" { - // No need to process top level. Avoid bug with helmignore .* matching - // empty names. See issue 1779. - return nil - } - - // Normalize to / since it will also work on Windows - n = filepath.ToSlash(n) - - if err != nil { - return err - } - if fi.IsDir() { - // Directory-based ignore rules should involve skipping the entire - // contents of that directory. - if rules.Ignore(n, fi) { - return filepath.SkipDir - } - return nil - } - - // If a .helmignore file matches, skip this file. - if rules.Ignore(n, fi) { - return nil - } - - // Irregular files include devices, sockets, and other uses of files that - // are not regular files. In Go they have a file mode type bit set. - // See https://golang.org/pkg/os/#FileMode for examples. - if !fi.Mode().IsRegular() { - return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) - } - - data, err := os.ReadFile(name) - if err != nil { - return errors.Wrapf(err, "error reading %s", n) - } - - data = bytes.TrimPrefix(data, utf8bom) - - files = append(files, &BufferedFile{Name: n, Data: data}) - return nil - } - if err = sympath.Walk(topdir, walk); err != nil { - return nil, err - } - - return files, nil -} diff --git a/pkg/helm/pkg/chart/loader/load.go b/pkg/helm/pkg/chart/loader/load.go index c5f264d9..d85b3b72 100644 --- a/pkg/helm/pkg/chart/loader/load.go +++ b/pkg/helm/pkg/chart/loader/load.go @@ -17,38 +17,36 @@ limitations under the License. package loader import ( - "bytes" + "compress/gzip" "context" + "errors" "fmt" - "log" + "io" "os" "path/filepath" - "strings" - "github.com/pkg/errors" "sigs.k8s.io/yaml" - "github.com/werf/common-go/pkg/secrets_manager" + nelmcommon "github.com/werf/nelm/pkg/common" + c3 "github.com/werf/nelm/pkg/helm/intern/chart/v3" + c3load "github.com/werf/nelm/pkg/helm/intern/chart/v3/loader" "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/werf/chartextender" - "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets" - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/runtimedata" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + c2 "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + c2load "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" ) // ChartLoader loads a chart. type ChartLoader interface { - Load(options helmopts.HelmOptions) (*chart.Chart, error) + Load(ctx context.Context) (chart.Charter, error) } // Loader returns a new ChartLoader appropriate for the given chart name func Loader(name string) (ChartLoader, error) { isDir, err := loader(name) if err != nil { - return nil, errors.Wrapf(err, "error checking if %s is a directory", name) + return nil, err } - if isDir { return DirLoader(name), nil } @@ -56,17 +54,16 @@ func Loader(name string) (ChartLoader, error) { } func loader(name string) (bool, error) { - if file.ChartFileReader == nil { + if nelmcommon.ChartFileReader == nil { fi, err := os.Stat(name) if err != nil { return false, err } - if fi.IsDir() { - return true, nil - } - return false, nil + + return fi.IsDir(), nil } - return file.ChartFileReader.ChartIsDir(name) + + return nelmcommon.ChartFileReader.ChartIsDir(name) } // Load takes a string name, tries to resolve it to a file or directory, and then loads it. @@ -75,288 +72,227 @@ func loader(name string) (bool, error) { // and hand off to the appropriate chart reader. // // If a .helmignore file is present, the directory loader will skip loading any files -// matching it. -func Load(name string, opts helmopts.HelmOptions) (*chart.Chart, error) { +// matching it. But .helmignore is not evaluated when reading out of an archive. +func Load(ctx context.Context, name string) (chart.Charter, error) { l, err := Loader(name) if err != nil { return nil, err } - return l.Load(opts) + + return l.Load(ctx) } -// BufferedFile represents an archive file buffered for later processing. -type BufferedFile struct { - Name string - Data []byte +// DirLoader loads a chart from a directory +type DirLoader string + +// Load loads the chart +func (l DirLoader) Load(ctx context.Context) (chart.Charter, error) { + return LoadDir(ctx, string(l)) } -// LoadFiles loads from in-memory files. -func LoadFiles(files []*BufferedFile, opts helmopts.HelmOptions) (*chart.Chart, error) { - c := new(chart.Chart) - subcharts := make(map[string][]*BufferedFile) +func LoadDir(ctx context.Context, dir string) (chart.Charter, error) { + if nelmcommon.HasHelmOptions(ctx) { + return loadDirWerf(ctx, dir) + } - c.SecretsRuntimeData = secrets.NewSecretsRuntimeData() + return loadDirVanilla(ctx, dir) +} - // do not rely on assumed ordering of files in the chart and crash - // if Chart.yaml was not coming early enough to initialize metadata - for _, f := range files { - c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data}) - if f.Name == "Chart.yaml" { - if c.Metadata == nil { - c.Metadata = new(chart.Metadata) - } - if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { - return c, errors.Wrap(err, "cannot load Chart.yaml") - } - // NOTE(bacongobbler): while the chart specification says that APIVersion must be set, - // Helm 2 accepted charts that did not provide an APIVersion in their chart metadata. - // Because of that, if APIVersion is unset, we should assume we're loading a v1 chart. - if c.Metadata.APIVersion == "" { - c.Metadata.APIVersion = chart.APIVersionV1 - } - } +func loadDirVanilla(ctx context.Context, dir string) (chart.Charter, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err } - for _, f := range files { - switch { - case f.Name == "Chart.yaml": - // already processed - continue - case f.Name == "Chart.lock": - c.Lock = new(chart.Lock) - if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { - return c, errors.Wrap(err, "cannot load Chart.lock") - } - case f.Name == "values.yaml": - c.Values = make(map[string]interface{}) - if err := yaml.Unmarshal(f.Data, &c.Values); err != nil { - return c, errors.Wrap(err, "cannot load values.yaml") - } - case f.Name == "values.schema.json": - c.Schema = f.Data - - // Deprecated: requirements.yaml is deprecated use Chart.yaml. - // We will handle it for you because we are nice people - case f.Name == "requirements.yaml": - if c.Metadata == nil { - c.Metadata = new(chart.Metadata) - } - if c.Metadata.APIVersion != chart.APIVersionV1 { - log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.") - } - if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { - return c, errors.Wrap(err, "cannot load requirements.yaml") - } - if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) - } - // Deprecated: requirements.lock is deprecated use Chart.lock. - case f.Name == "requirements.lock": - c.Lock = new(chart.Lock) - if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { - return c, errors.Wrap(err, "cannot load requirements.lock") - } - if c.Metadata == nil { - c.Metadata = new(chart.Metadata) - } - if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) - } - case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data}) - case strings.HasPrefix(f.Name, "charts/"): - if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) - continue - } + name := filepath.Join(topdir, "Chart.yaml") - fname := strings.TrimPrefix(f.Name, "charts/") - cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) - case strings.HasPrefix(f.Name, "ts/") && !strings.HasPrefix(f.Name, "ts/node_modules/"): - c.RuntimeFiles = append(c.RuntimeFiles, &chart.File{Name: f.Name, Data: f.Data}) - default: - c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) - } + data, err := os.ReadFile(name) + if err != nil { + return nil, fmt.Errorf("unable to detect chart at %s: %w", name, err) } - switch opts.ChartLoadOpts.ChartType { - case helmopts.ChartTypeBundle: - c.ExtraValues = opts.ChartLoadOpts.ExtraValues - - if !opts.ChartLoadOpts.NoSecrets { - if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( - context.Background(), - convertBufferedFilesForChartExtender(files), - secrets_manager.Manager, - runtimedata.DecodeAndLoadSecretsOptions{ - CustomSecretValueFiles: opts.ChartLoadOpts.SecretValuesFiles, - LoadFromLocalFilesystem: true, - NoDecryptSecrets: opts.ChartLoadOpts.SecretKeyIgnore, - SecretsWorkingDir: opts.ChartLoadOpts.SecretWorkDir, - WithoutDefaultSecretValues: opts.ChartLoadOpts.DefaultSecretValuesDisable, - }, - ); err != nil { - return nil, fmt.Errorf("error decoding secrets: %w", err) - } - } + c := new(chartBase) + if err = yaml.Unmarshal(data, c); err != nil { + return nil, fmt.Errorf("cannot load Chart.yaml: %w", err) + } - if opts.ChartLoadOpts.DefaultValuesDisable { - c.Values = nil - } - case helmopts.ChartTypeChart: - c.ExtraValues = opts.ChartLoadOpts.ExtraValues - - if !opts.ChartLoadOpts.NoSecrets { - if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( - context.Background(), - convertBufferedFilesForChartExtender(files), - secrets_manager.Manager, - runtimedata.DecodeAndLoadSecretsOptions{ - CustomSecretValueFiles: opts.ChartLoadOpts.SecretValuesFiles, - LoadFromLocalFilesystem: file.ChartFileReader == nil, - NoDecryptSecrets: opts.ChartLoadOpts.SecretKeyIgnore, - SecretsWorkingDir: opts.ChartLoadOpts.SecretWorkDir, - WithoutDefaultSecretValues: opts.ChartLoadOpts.DefaultSecretValuesDisable, - }, - ); err != nil { - return nil, fmt.Errorf("error decoding secrets: %w", err) + switch c.APIVersion { + case c2.APIVersionV1, c2.APIVersionV2, "": + return c2load.Load(ctx, dir) + case c3.APIVersionV3: + return c3load.Load(ctx, dir) + default: + return nil, errors.New("unsupported chart version") + } +} + +func loadDirWerf(ctx context.Context, dir string) (chart.Charter, error) { + helmOpts := nelmcommon.HelmOptionsFromContext(ctx) + + var chartFiles []*nelmcommon.BufferedFile + switch helmOpts.ChartLoadOpts.ChartType { + case nelmcommon.LegacyChartTypeChart: + if nelmcommon.ChartFileReader != nil { + var err error + chartFiles, err = nelmcommon.ChartFileReader.LoadChartDir(ctx, dir) + if err != nil { + return nil, fmt.Errorf("load chart dir: %w", err) + } + } else { + localFiles, err := getFilesFromLocalFilesystem(dir) + if err != nil { + return nil, fmt.Errorf("load chart dir from filesystem: %w", err) } - } - c.Metadata = chartextender.AutosetChartMetadata( - c.Metadata, - chartextender.GetHelmChartMetadataOptions{ - DefaultAPIVersion: opts.ChartLoadOpts.DefaultChartAPIVersion, - DefaultName: opts.ChartLoadOpts.DefaultChartName, - DefaultVersion: opts.ChartLoadOpts.DefaultChartVersion, - OverrideAppVersion: opts.ChartLoadOpts.ChartAppVersion, - }, - ) - - c.Templates = append(c.Templates, &chart.File{ - Name: "templates/_werf_helpers.tpl", - }) - - if opts.ChartLoadOpts.DefaultValuesDisable { - c.Values = nil + chartFiles = localFiles } - case helmopts.ChartTypeSubchart: - if !opts.ChartLoadOpts.NoSecrets { - if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( - context.Background(), - convertBufferedFilesForChartExtender(files), - secrets_manager.Manager, - runtimedata.DecodeAndLoadSecretsOptions{ - LoadFromLocalFilesystem: file.ChartFileReader == nil, - NoDecryptSecrets: opts.ChartLoadOpts.SecretKeyIgnore, - SecretsWorkingDir: opts.ChartLoadOpts.SecretWorkDir, - WithoutDefaultSecretValues: opts.ChartLoadOpts.DefaultSecretValuesDisable, - }, - ); err != nil { - return nil, fmt.Errorf("error decoding secrets: %w", err) - } + case nelmcommon.LegacyChartTypeBundle, nelmcommon.LegacyChartTypeSubchart, nelmcommon.LegacyChartTypeChartStub: + localFiles, err := getFilesFromLocalFilesystem(dir) + if err != nil { + return nil, fmt.Errorf("load chart dir from filesystem: %w", err) } - case helmopts.ChartTypeChartStub: - if !opts.ChartLoadOpts.NoSecrets { - if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( - context.Background(), - convertBufferedFilesForChartExtender(files), - secrets_manager.Manager, - runtimedata.DecodeAndLoadSecretsOptions{ - LoadFromLocalFilesystem: true, - NoDecryptSecrets: opts.ChartLoadOpts.SecretKeyIgnore, - SecretsWorkingDir: opts.ChartLoadOpts.SecretWorkDir, - WithoutDefaultSecretValues: opts.ChartLoadOpts.DefaultSecretValuesDisable, - }, - ); err != nil { - return nil, fmt.Errorf("error decoding secrets: %w", err) + + chartFiles = localFiles + default: + return nil, fmt.Errorf("unexpected chart type: %q", helmOpts.ChartLoadOpts.ChartType) + } + + switch helmOpts.ChartLoadOpts.ChartType { + case nelmcommon.LegacyChartTypeChart, nelmcommon.LegacyChartTypeBundle: + var loadChartDirFunc func(ctx context.Context, dir string) ([]*nelmcommon.BufferedFile, error) + if nelmcommon.ChartFileReader != nil { + loadChartDirFunc = nelmcommon.ChartFileReader.LoadChartDir + } else { + loadChartDirFunc = func(ctx context.Context, dir string) ([]*nelmcommon.BufferedFile, error) { + return getFilesFromLocalFilesystem(dir) } } - c.Metadata = chartextender.AutosetChartMetadata( - c.Metadata, - chartextender.GetHelmChartMetadataOptions{ - DefaultAPIVersion: chart.APIVersionV2, - DefaultName: "stubchartname", - DefaultVersion: "1.0.0", - }, - ) - - c.Templates = append(c.Templates, &chart.File{ - Name: "templates/_werf_helpers.tpl", - }) - default: - panic("unexpected type") + var err error + chartFiles, err = LoadChartDependencies(ctx, loadChartDirFunc, dir, chartFiles, helmOpts) + if err != nil { + return nil, fmt.Errorf("load chart dependencies: %w", err) + } } - if c.Metadata == nil { - return c, errors.New("Chart.yaml file is missing") + files := make([]*archive.BufferedFile, 0, len(chartFiles)) + for _, f := range chartFiles { + files = append(files, &archive.BufferedFile{Name: f.Name, Data: f.Data}) } - if err := c.Validate(); err != nil { - return c, err + apiVersion := detectAPIVersion(chartFiles) + + switch apiVersion { + case c2.APIVersionV1, c2.APIVersionV2, "": + return c2load.LoadFiles(ctx, files) + case c3.APIVersionV3: + return c3load.LoadFiles(ctx, files) + default: + return nil, fmt.Errorf("unsupported chart version: %s", apiVersion) } +} - for n, files := range subcharts { - var sc *chart.Chart - var err error - switch { - case strings.IndexAny(n, "_.") == 0: - continue - case filepath.Ext(n) == ".tgz": - file := files[0] - if file.Name != n { - return c, errors.Errorf("error unpacking tar in %s: expected %s, got %s", c.Name(), n, file.Name) +func detectAPIVersion(files []*nelmcommon.BufferedFile) string { + for _, f := range files { + if f.Name == "Chart.yaml" { + c := new(chartBase) + if err := yaml.Unmarshal(f.Data, c); err == nil { + return c.APIVersion } + } + } - opts.ChartLoadOpts.ChartType = helmopts.ChartTypeSubchart - - // Untar the chart and add to c.Dependencies - sc, err = LoadArchive(bytes.NewBuffer(file.Data), opts) - default: - // We have to trim the prefix off of every file, and ignore any file - // that is in charts/, but isn't actually a chart. - buff := make([]*BufferedFile, 0, len(files)) - for _, f := range files { - parts := strings.SplitN(f.Name, "/", 2) - if len(parts) < 2 { - continue - } - f.Name = parts[1] - buff = append(buff, f) - } + return "" +} + +// FileLoader loads a chart from a file +type FileLoader string + +// Load loads a chart +func (l FileLoader) Load(ctx context.Context) (chart.Charter, error) { + return LoadFile(ctx, string(l)) +} + +func LoadFile(ctx context.Context, name string) (chart.Charter, error) { + if fi, err := os.Stat(name); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory") + } + + raw, err := os.Open(name) + if err != nil { + return nil, err + } + defer raw.Close() - opts.ChartLoadOpts.ChartType = helmopts.ChartTypeSubchart + err = archive.EnsureArchive(name, raw) + if err != nil { + return nil, err + } - sc, err = LoadFiles(buff, opts) + files, err := archive.LoadArchiveFiles(raw) + if err != nil { + if errors.Is(err, gzip.ErrHeader) { + return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", name, err) } + return nil, errors.New("unable to load chart archive") + } - if err != nil { - return c, errors.Wrapf(err, "error unpacking %s in %s", n, c.Name()) + for _, f := range files { + if f.Name == "Chart.yaml" { + c := new(chartBase) + if err := yaml.Unmarshal(f.Data, c); err != nil { + return c, fmt.Errorf("cannot load Chart.yaml: %w", err) + } + switch c.APIVersion { + case c2.APIVersionV1, c2.APIVersionV2, "": + return c2load.Load(ctx, name) + case c3.APIVersionV3: + return c3load.Load(ctx, name) + default: + return nil, errors.New("unsupported chart version") + } } - c.AddDependency(sc) } - return c, nil + return nil, errors.New("unable to detect chart version, no Chart.yaml found") } -func convertBufferedFilesForChartExtender(files []*BufferedFile) []*file.ChartExtenderBufferedFile { - var res []*file.ChartExtenderBufferedFile - for _, f := range files { - f1 := new(file.ChartExtenderBufferedFile) - *f1 = file.ChartExtenderBufferedFile(*f) - res = append(res, f1) +// LoadArchive loads from a reader containing a compressed tar archive. +func LoadArchive(ctx context.Context, in io.Reader) (chart.Charter, error) { + // Note: This function is for use by SDK users such as Flux. + + files, err := archive.LoadArchiveFiles(in) + if err != nil { + if errors.Is(err, gzip.ErrHeader) { + return nil, fmt.Errorf("stream does not appear to be a valid chart file (details: %w)", err) + } + return nil, fmt.Errorf("unable to load chart archive: %w", err) } - return res -} -func convertChartExtenderFilesToBufferedFiles(files []*file.ChartExtenderBufferedFile) []*BufferedFile { - var res []*BufferedFile for _, f := range files { - f1 := new(BufferedFile) - *f1 = BufferedFile(*f) - res = append(res, f1) + if f.Name == "Chart.yaml" { + c := new(chartBase) + if err := yaml.Unmarshal(f.Data, c); err != nil { + return c, fmt.Errorf("cannot load Chart.yaml: %w", err) + } + switch c.APIVersion { + case c2.APIVersionV1, c2.APIVersionV2, "": + return c2load.LoadFiles(ctx, files) + case c3.APIVersionV3: + return c3load.LoadFiles(ctx, files) + default: + return nil, errors.New("unsupported chart version") + } + } } - return res + + return nil, errors.New("unable to detect chart version, no Chart.yaml found") +} + +// chartBase is used to detect the API Version for the chart to run it through the +// loader for that type. +type chartBase struct { + APIVersion string `json:"apiVersion,omitempty"` } diff --git a/pkg/helm/pkg/chart/loader/load_dependencies.go b/pkg/helm/pkg/chart/loader/load_dependencies.go index 362db0e8..eb572201 100644 --- a/pkg/helm/pkg/chart/loader/load_dependencies.go +++ b/pkg/helm/pkg/chart/loader/load_dependencies.go @@ -8,23 +8,26 @@ import ( "encoding/json" "fmt" "io" - "io/ioutil" "os" "path/filepath" "strings" "github.com/google/uuid" - "github.com/pkg/errors" "sigs.k8s.io/yaml" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" "github.com/werf/common-go/pkg/locker" "github.com/werf/common-go/pkg/util" "github.com/werf/lockgate" + nelmcommon "github.com/werf/nelm/pkg/common" + "github.com/werf/nelm/pkg/helm/intern/sympath" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/ignore" + "github.com/werf/nelm/pkg/log" ) +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + var localCacheDir string var serviceDir string @@ -66,11 +69,11 @@ func SetServiceDir(dir string) { func LoadChartDependencies( ctx context.Context, - loadChartDirFunc func(ctx context.Context, dir string) ([]*file.ChartExtenderBufferedFile, error), + loadChartDirFunc func(ctx context.Context, dir string) ([]*nelmcommon.BufferedFile, error), chartDir string, - loadedChartFiles []*file.ChartExtenderBufferedFile, - opts helmopts.HelmOptions, -) ([]*file.ChartExtenderBufferedFile, error) { + loadedChartFiles []*nelmcommon.BufferedFile, + opts nelmcommon.HelmOptions, +) ([]*nelmcommon.BufferedFile, error) { res := loadedChartFiles var chartMetadata *chart.Metadata @@ -81,7 +84,7 @@ func LoadChartDependencies( case "Chart.yaml": chartMetadata = new(chart.Metadata) if err := yaml.Unmarshal(f.Data, chartMetadata); err != nil { - return nil, errors.Wrap(err, "cannot load Chart.yaml") + return nil, fmt.Errorf("cannot load Chart.yaml: %w", err) } if chartMetadata.APIVersion == "" { chartMetadata.APIVersion = chart.APIVersionV1 @@ -90,7 +93,7 @@ func LoadChartDependencies( case "Chart.lock": chartMetadataLock = new(chart.Lock) if err := yaml.Unmarshal(f.Data, chartMetadataLock); err != nil { - return nil, errors.Wrap(err, "cannot load Chart.lock") + return nil, fmt.Errorf("cannot load Chart.lock: %w", err) } } } @@ -102,7 +105,7 @@ func LoadChartDependencies( chartMetadata = new(chart.Metadata) } if err := yaml.Unmarshal(f.Data, chartMetadata); err != nil { - return nil, errors.Wrap(err, "cannot load requirements.yaml") + return nil, fmt.Errorf("cannot load requirements.yaml: %w", err) } case "requirements.lock": @@ -110,7 +113,7 @@ func LoadChartDependencies( chartMetadataLock = new(chart.Lock) } if err := yaml.Unmarshal(f.Data, chartMetadataLock); err != nil { - return nil, errors.Wrap(err, "cannot load requirements.lock") + return nil, fmt.Errorf("cannot load requirements.lock: %w", err) } } } @@ -121,8 +124,7 @@ func LoadChartDependencies( if chartMetadataLock == nil { if len(chartMetadata.Dependencies) > 0 && NoChartLockWarning != "" { - // TODO(werf): move logger to common-go and use it - fmt.Println(NoChartLockWarning) + log.Default.Warn(ctx, "%s", NoChartLockWarning) } return res, nil @@ -130,8 +132,6 @@ func LoadChartDependencies( conf := newChartDependenciesConfiguration(chartMetadata, chartMetadataLock) - // Append virtually loaded files from custom dependency repositories in the local filesystem, - // pretending these files are located in the charts/ dir as designed in the Helm. for _, chartDep := range chartMetadataLock.Dependencies { if !strings.HasPrefix(chartDep.Repository, "file://") { continue @@ -164,15 +164,15 @@ func LoadChartDependencies( if err != nil { return nil, fmt.Errorf("error preparing chart dependencies: %w", err) } - localFiles, err := GetFilesFromLocalFilesystem(depsDir) + localFiles, err := getFilesFromLocalFilesystem(depsDir) if err != nil { return nil, err } for _, f := range localFiles { if strings.HasPrefix(f.Name, "charts/") { - f1 := new(file.ChartExtenderBufferedFile) - *f1 = file.ChartExtenderBufferedFile(*f) + f1 := new(nelmcommon.BufferedFile) + *f1 = nelmcommon.BufferedFile(*f) res = append(res, f1) } } @@ -231,7 +231,6 @@ func prepareDependenciesDir(ctx context.Context, metadataBytes, metadataLockByte case err != nil: return fmt.Errorf("error accessing %s: %w", depsDir, err) default: - // at the time we have acquired a lock the target directory was created return nil } @@ -264,18 +263,18 @@ func createChartDependenciesDir(destDir string, metadataBytes, metadataLockBytes return fmt.Errorf("error creating dir %q: %w", destDir, err) } - files := []*file.ChartExtenderBufferedFile{ + files := []*nelmcommon.BufferedFile{ {Name: "Chart.yaml", Data: metadataBytes}, {Name: "Chart.lock", Data: metadataLockBytes}, } - for _, file := range files { - if file == nil { + for _, f := range files { + if f == nil { continue } - path := filepath.Join(destDir, file.Name) - if err := ioutil.WriteFile(path, file.Data, 0o644); err != nil { + path := filepath.Join(destDir, f.Name) + if err := os.WriteFile(path, f.Data, 0o644); err != nil { return fmt.Errorf("error writing %q: %w", path, err) } } @@ -283,7 +282,7 @@ func createChartDependenciesDir(destDir string, metadataBytes, metadataLockBytes return nil } -func getPreparedChartDependenciesDir(ctx context.Context, metadataFile, metadataLockFile *file.ChartExtenderBufferedFile, opts helmopts.HelmOptions) (string, error) { +func getPreparedChartDependenciesDir(ctx context.Context, metadataFile, metadataLockFile *nelmcommon.BufferedFile, opts nelmcommon.HelmOptions) (string, error) { return prepareDependenciesDir(ctx, metadataFile.Data, metadataLockFile.Data, func(tmpDepsDir string) error { if err := buildChartDependenciesInDir(ctx, tmpDepsDir, opts); err != nil { return fmt.Errorf("error building chart dependencies: %w", err) @@ -301,7 +300,7 @@ func newChartDependenciesConfiguration(chartMetadata *chart.Metadata, chartMetad return &chartDependenciesConfiguration{ChartMetadata: chartMetadata, ChartMetadataLock: chartMetadataLock} } -func (conf *chartDependenciesConfiguration) GetExternalDependenciesFiles(loadedChartFiles []*file.ChartExtenderBufferedFile) (bool, *file.ChartExtenderBufferedFile, *file.ChartExtenderBufferedFile, error) { +func (conf *chartDependenciesConfiguration) GetExternalDependenciesFiles(loadedChartFiles []*nelmcommon.BufferedFile) (bool, *nelmcommon.BufferedFile, *nelmcommon.BufferedFile, error) { metadataBytes, err := yaml.Marshal(conf.ChartMetadata) if err != nil { return false, nil, nil, fmt.Errorf("unable to marshal original chart metadata into yaml: %w", err) @@ -320,7 +319,7 @@ func (conf *chartDependenciesConfiguration) GetExternalDependenciesFiles(loadedC return false, nil, nil, fmt.Errorf("unable to unmarshal original chart metadata lock yaml: %w", err) } - metadata.APIVersion = "v2" + metadata.APIVersion = chart.APIVersionV2 var externalDependenciesNames []string isExternalDependency := func(depName string) bool { @@ -329,6 +328,7 @@ func (conf *chartDependenciesConfiguration) GetExternalDependenciesFiles(loadedC return true } } + return false } @@ -369,7 +369,6 @@ FindExternalDependencies: return false, nil, nil, nil } - // Set resolved repository from the lock file for _, dep := range metadata.Dependencies { for _, depLock := range metadataLock.Dependencies { if dep.Name == depLock.Name { @@ -385,14 +384,14 @@ FindExternalDependencies: metadataLock.Digest = newDigest } - metadataFile := &file.ChartExtenderBufferedFile{Name: "Chart.yaml"} + metadataFile := &nelmcommon.BufferedFile{Name: "Chart.yaml"} if data, err := yaml.Marshal(metadata); err != nil { return false, nil, nil, fmt.Errorf("unable to marshal chart metadata file with external dependencies: %w", err) } else { metadataFile.Data = data } - metadataLockFile := &file.ChartExtenderBufferedFile{Name: "Chart.lock"} + metadataLockFile := &nelmcommon.BufferedFile{Name: "Chart.lock"} if data, err := yaml.Marshal(metadataLock); err != nil { return false, nil, nil, fmt.Errorf("unable to marshal chart metadata lock file with external dependencies: %w", err) } else { @@ -407,15 +406,21 @@ func hashReq(req, lock []*chart.Dependency) (string, error) { if err != nil { return "", err } + s, err := digest(bytes.NewBuffer(data)) - return "sha256:" + s, err + if err != nil { + return "", err + } + + return "sha256:" + s, nil } func digest(in io.Reader) (string, error) { hash := crypto.SHA256.New() if _, err := io.Copy(hash, in); err != nil { - return "", nil + return "", err } + return hex.EncodeToString(hash.Sum(nil)), nil } @@ -423,9 +428,85 @@ func makeDependencyArchiveName(depName, depVersion string) string { return fmt.Sprintf("%s-%s.tgz", depName, depVersion) } -func buildChartDependenciesInDir(ctx context.Context, targetDir string, opts helmopts.HelmOptions) error { - opts.ChartLoadOpts.ChartType = helmopts.ChartTypeChartStub - opts.ChartLoadOpts.DepDownloader.SetChartPath(targetDir) +func buildChartDependenciesInDir(ctx context.Context, targetDir string, opts nelmcommon.HelmOptions) error { + if opts.ChartLoadOpts.ChartDepsDownloader == nil { + return fmt.Errorf("dependency downloader is required") + } + + opts.ChartLoadOpts.ChartType = nelmcommon.LegacyChartTypeChartStub + opts.ChartLoadOpts.ChartDepsDownloader.SetChartPath(targetDir) + ctx = nelmcommon.ContextWithHelmOptions(ctx, opts) + + if err := opts.ChartLoadOpts.ChartDepsDownloader.Build(ctx); err != nil { + return fmt.Errorf("build dependencies: %w", err) + } + + return nil +} + +func getFilesFromLocalFilesystem(dir string) ([]*nelmcommon.BufferedFile, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + rules := ignore.Empty() + ifile := filepath.Join(topdir, ignore.HelmIgnore) + if _, err := os.Stat(ifile); err == nil { + r, err := ignore.ParseFile(ifile) + if err != nil { + return nil, err + } + rules = r + } + rules.AddDefaults() + + var files []*nelmcommon.BufferedFile + topdir += string(filepath.Separator) + + walk := func(name string, fi os.FileInfo, err error) error { + n := strings.TrimPrefix(name, topdir) + if n == "" { + return nil + } + + n = filepath.ToSlash(n) + + if err != nil { + return err + } + if fi.IsDir() { + if rules.Ignore(n, fi) { + return filepath.SkipDir + } + return nil + } + + if rules.Ignore(n, fi) { + return nil + } + + if !fi.Mode().IsRegular() { + return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) + } + + if fi.Size() > archive.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) + } + + data, err := os.ReadFile(name) + if err != nil { + return fmt.Errorf("error reading %s: %w", n, err) + } + + data = bytes.TrimPrefix(data, utf8bom) + + files = append(files, &nelmcommon.BufferedFile{Name: n, Data: data}) + return nil + } + if err = sympath.Walk(topdir, walk); err != nil { + return nil, err + } - return opts.ChartLoadOpts.DepDownloader.Build(opts) + return files, nil } diff --git a/pkg/helm/pkg/chart/loader/load_test.go b/pkg/helm/pkg/chart/loader/load_test.go index 93b8d7b3..16cdcf2b 100644 --- a/pkg/helm/pkg/chart/loader/load_test.go +++ b/pkg/helm/pkg/chart/loader/load_test.go @@ -20,719 +20,168 @@ import ( "archive/tar" "bytes" "compress/gzip" + "context" + "fmt" "io" - "log" - "os" + "maps" "path/filepath" - "runtime" "strings" "testing" "time" + c3 "github.com/werf/nelm/pkg/helm/intern/chart/v3" "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + c2 "github.com/werf/nelm/pkg/helm/pkg/chart/v2" ) -// GlobalLoadOptions is a default set of options for testing -var GlobalLoadOptions = &helmopts.HelmOptions{} - -func TestLoadDir(t *testing.T) { - l, err := Loader("testdata/frobnitz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyDependencies(t, c) - verifyDependenciesLock(t, c) -} - -func TestLoadDirWithDevNull(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("test only works on unix systems with /dev/null present") - } - - l, err := Loader("testdata/frobnitz_with_dev_null") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - if _, err := l.Load(*GlobalLoadOptions); err == nil { - t.Errorf("packages with an irregular file (/dev/null) should not load") - } -} - -func TestLoadDirWithSymlink(t *testing.T) { - sym := filepath.Join("..", "LICENSE") - link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE") - - if err := os.Symlink(sym, link); err != nil { - t.Fatal(err) - } - - defer os.Remove(link) - - l, err := Loader("testdata/frobnitz_with_symlink") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyDependencies(t, c) - verifyDependenciesLock(t, c) -} - -func TestBomTestData(t *testing.T) { - testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} - for _, file := range testFiles { - data, err := os.ReadFile("testdata/" + file) - if err != nil || !bytes.HasPrefix(data, utf8bom) { - t.Errorf("Test file has no BOM or is invalid: testdata/%s", file) - } - } - - archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz") - if err != nil { - t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) - } - unzipped, err := gzip.NewReader(bytes.NewReader(archive)) - if err != nil { - t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) - } - defer unzipped.Close() - for _, testFile := range testFiles { - data := make([]byte, 3) - err := unzipped.Reset(bytes.NewReader(archive)) - if err != nil { - t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) +// createChartArchive is a helper function to create a gzipped tar archive in memory +func createChartArchive(t *testing.T, chartName, apiVersion string, extraFiles map[string][]byte, createChartYaml bool) io.Reader { + t.Helper() + var buf bytes.Buffer + gw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gw) + + files := make(map[string][]byte) + maps.Copy(files, extraFiles) + + if createChartYaml { + chartYAMLContent := fmt.Sprintf(`apiVersion: %s +name: %s +version: 0.1.0 +description: A test chart +`, apiVersion, chartName) + files["Chart.yaml"] = []byte(chartYAMLContent) + } + + for name, data := range files { + header := &tar.Header{ + Name: filepath.Join(chartName, name), + Mode: 0644, + Size: int64(len(data)), + ModTime: time.Now(), } - tr := tar.NewReader(unzipped) - for { - file, err := tr.Next() - if err == io.EOF { - break - } - if err != nil { - t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) - } - if file != nil && strings.EqualFold(file.Name, testFile) { - _, err := tr.Read(data) - if err != nil { - t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) - } else { - break - } - } + if err := tw.WriteHeader(header); err != nil { + t.Fatalf("Failed to write tar header for %s: %v", name, err) } - if !bytes.Equal(data, utf8bom) { - t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile) + if _, err := tw.Write(data); err != nil { + t.Fatalf("Failed to write tar data for %s: %v", name, err) } } -} - -func TestLoadDirWithUTFBOM(t *testing.T) { - l, err := Loader("testdata/frobnitz_with_bom") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyDependencies(t, c) - verifyDependenciesLock(t, c) - verifyBomStripped(t, c.Files) -} - -func TestLoadArchiveWithUTFBOM(t *testing.T) { - l, err := Loader("testdata/frobnitz_with_bom.tgz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyDependencies(t, c) - verifyDependenciesLock(t, c) - verifyBomStripped(t, c.Files) -} -func TestLoadV1(t *testing.T) { - l, err := Loader("testdata/frobnitz.v1") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) + if err := tw.Close(); err != nil { + t.Fatalf("Failed to close tar writer: %v", err) } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) + if err := gw.Close(); err != nil { + t.Fatalf("Failed to close gzip writer: %v", err) } - verifyDependencies(t, c) - verifyDependenciesLock(t, c) + return &buf } -func TestLoadFileV1(t *testing.T) { - l, err := Loader("testdata/frobnitz.v1.tgz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyDependencies(t, c) - verifyDependenciesLock(t, c) -} - -func TestLoadFile(t *testing.T) { - l, err := Loader("testdata/frobnitz-1.2.3.tgz") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyFrobnitz(t, c) - verifyChart(t, c) - verifyDependencies(t, c) -} - -func TestLoadFiles_BadCases(t *testing.T) { - for _, tt := range []struct { - name string - bufferedFiles []*BufferedFile - expectError string +func TestLoadArchive(t *testing.T) { + testCases := []struct { + name string + chartName string + apiVersion string + extraFiles map[string][]byte + inputReader io.Reader + expectedChart chart.Charter + expectedError string + createChartYaml bool }{ { - name: "These files contain only requirements.lock", - bufferedFiles: []*BufferedFile{ - { - Name: "requirements.lock", - Data: []byte(""), - }, + name: "valid v2 chart archive", + chartName: "mychart-v2", + apiVersion: c2.APIVersionV2, + extraFiles: map[string][]byte{"templates/config.yaml": []byte("key: value")}, + expectedChart: &c2.Chart{ + Metadata: &c2.Metadata{APIVersion: c2.APIVersionV2, Name: "mychart-v2", Version: "0.1.0", Description: "A test chart"}, }, - expectError: "validation: chart.metadata.apiVersion is required"}, - } { - _, err := LoadFiles(tt.bufferedFiles, *GlobalLoadOptions) - if err == nil { - t.Fatal("expected error when load illegal files") - } - if !strings.Contains(err.Error(), tt.expectError) { - t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.name) - } - } -} - -func TestLoadFiles(t *testing.T) { - goodFiles := []*BufferedFile{ - { - Name: "Chart.yaml", - Data: []byte(`apiVersion: v1 -name: frobnitz -description: This is a frobnitz. -version: "1.2.3" -keywords: - - frobnitz - - sprocket - - dodad -maintainers: - - name: The Helm Team - email: helm@example.com - - name: Someone Else - email: nobody@example.com -sources: - - https://example.com/foo/bar -home: http://example.com -icon: https://example.com/64x64.png -`), - }, - { - Name: "values.yaml", - Data: []byte("var: some values"), - }, - { - Name: "values.schema.json", - Data: []byte("type: Values"), - }, - { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + createChartYaml: true, }, { - Name: "templates/service.yaml", - Data: []byte("some service"), - }, - } - - c, err := LoadFiles(goodFiles, *GlobalLoadOptions) - if err != nil { - t.Errorf("Expected good files to be loaded, got %v", err) - } - - if c.Name() != "frobnitz" { - t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name()) - } - - if c.Values["var"] != "some values" { - t.Error("Expected chart values to be populated with default values") - } - - if len(c.Raw) != 5 { - t.Errorf("Expected %d files, got %d", 5, len(c.Raw)) - } - - if !bytes.Equal(c.Schema, []byte("type: Values")) { - t.Error("Expected chart schema to be populated with default values") - } - - if len(c.Templates) != 2 { - t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) - } - - if _, err = LoadFiles([]*BufferedFile{}, *GlobalLoadOptions); err == nil { - t.Fatal("Expected err to be non-nil") - } - if err.Error() != "Chart.yaml file is missing" { - t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) - } -} - -// Test the order of file loading. The Chart.yaml file needs to come first for -// later comparison checks. See https://github.com/helm/helm/pull/8948 -func TestLoadFilesOrder(t *testing.T) { - goodFiles := []*BufferedFile{ - { - Name: "requirements.yaml", - Data: []byte("dependencies:"), + name: "valid v3 chart archive", + chartName: "mychart-v3", + apiVersion: c3.APIVersionV3, + extraFiles: map[string][]byte{"templates/config.yaml": []byte("key: value")}, + expectedChart: &c3.Chart{ + Metadata: &c3.Metadata{APIVersion: c3.APIVersionV3, Name: "mychart-v3", Version: "0.1.0", Description: "A test chart"}, + }, + createChartYaml: true, }, { - Name: "values.yaml", - Data: []byte("var: some values"), + name: "invalid gzip header", + inputReader: bytes.NewBufferString("not a gzip file"), + expectedError: "stream does not appear to be a valid chart file (details: gzip: invalid header)", }, - { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + name: "archive without Chart.yaml", + chartName: "no-chart-yaml", + apiVersion: c2.APIVersionV2, // This will be ignored as Chart.yaml is missing + extraFiles: map[string][]byte{"values.yaml": []byte("foo: bar")}, + expectedError: "unable to detect chart version, no Chart.yaml found", + createChartYaml: false, }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + name: "archive with malformed Chart.yaml", + chartName: "malformed-chart-yaml", + apiVersion: c2.APIVersionV2, + extraFiles: map[string][]byte{"Chart.yaml": []byte("apiVersion: v2\nname: mychart\nversion: 0.1.0\ndescription: A test chart\ninvalid: :")}, + expectedError: "cannot load Chart.yaml: error converting YAML to JSON: yaml: line 5: mapping values are not allowed in this context", + createChartYaml: false, }, { - Name: "Chart.yaml", - Data: []byte(`apiVersion: v1 -name: frobnitz -description: This is a frobnitz. -version: "1.2.3" -keywords: - - frobnitz - - sprocket - - dodad -maintainers: - - name: The Helm Team - email: helm@example.com - - name: Someone Else - email: nobody@example.com -sources: - - https://example.com/foo/bar -home: http://example.com -icon: https://example.com/64x64.png -`), + name: "unsupported API version", + chartName: "unsupported-api", + apiVersion: "v99", + expectedError: "unsupported chart version", + createChartYaml: true, }, } - // Capture stderr to make sure message about Chart.yaml handle dependencies - // is not present - r, w, err := os.Pipe() - if err != nil { - t.Fatalf("Unable to create pipe: %s", err) - } - stderr := log.Writer() - log.SetOutput(w) - defer func() { - log.SetOutput(stderr) - }() - - _, err = LoadFiles(goodFiles, *GlobalLoadOptions) - if err != nil { - t.Errorf("Expected good files to be loaded, got %v", err) - } - w.Close() - - var text bytes.Buffer - io.Copy(&text, r) - if text.String() != "" { - t.Errorf("Expected no message to Stderr, got %s", text.String()) - } - -} - -// Packaging the chart on a Windows machine will produce an -// archive that has \\ as delimiters. Test that we support these archives -func TestLoadFileBackslash(t *testing.T) { - c, err := Load("testdata/frobnitz_backslash-1.2.3.tgz", *GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyChartFileAndTemplate(t, c, "frobnitz_backslash") - verifyChart(t, c) - verifyDependencies(t, c) -} - -func TestLoadV2WithReqs(t *testing.T) { - l, err := Loader("testdata/frobnitz.v2.reqs") - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - c, err := l.Load(*GlobalLoadOptions) - if err != nil { - t.Fatalf("Failed to load testdata: %s", err) - } - verifyDependencies(t, c) - verifyDependenciesLock(t, c) -} - -func TestLoadInvalidArchive(t *testing.T) { - tmpdir := t.TempDir() - - writeTar := func(filename, internalPath string, body []byte) { - dest, err := os.Create(filename) - if err != nil { - t.Fatal(err) - } - zipper := gzip.NewWriter(dest) - tw := tar.NewWriter(zipper) - - h := &tar.Header{ - Name: internalPath, - Mode: 0755, - Size: int64(len(body)), - ModTime: time.Now(), - } - if err := tw.WriteHeader(h); err != nil { - t.Fatal(err) - } - if _, err := tw.Write(body); err != nil { - t.Fatal(err) - } - tw.Close() - zipper.Close() - dest.Close() - } - - for _, tt := range []struct { - chartname string - internal string - expectError string - }{ - {"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"}, - {"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"}, - {"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"}, - {"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"}, - {"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"}, - {"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"}, - {"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"}, - {"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"}, - {"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"}, - {"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"}, - {"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"}, - {"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"}, - - // Under special circumstances, this can get normalized to things that look like absolute Windows paths - {"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"}, - {"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"}, - {"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"}, - } { - illegalChart := filepath.Join(tmpdir, tt.chartname) - writeTar(illegalChart, tt.internal, []byte("hello: world")) - _, err := Load(illegalChart, *GlobalLoadOptions) - if err == nil { - t.Fatal("expected error when unpacking illegal files") - } - if !strings.Contains(err.Error(), tt.expectError) { - t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname) - } - } - - // Make sure that absolute path gets interpreted as relative - illegalChart := filepath.Join(tmpdir, "abs-path.tgz") - writeTar(illegalChart, "/Chart.yaml", []byte("hello: world")) - _, err := Load(illegalChart, *GlobalLoadOptions) - if err.Error() != "validation: chart.metadata.name is required" { - t.Error(err) - } - - // And just to validate that the above was not spurious - illegalChart = filepath.Join(tmpdir, "abs-path2.tgz") - writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world")) - _, err = Load(illegalChart, *GlobalLoadOptions) - if err.Error() != "Chart.yaml file is missing" { - t.Errorf("Unexpected error message: %s", err) - } - - // Finally, test that drive letter gets stripped off on Windows - illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz") - writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world")) - _, err = Load(illegalChart, *GlobalLoadOptions) - if err.Error() != "validation: chart.metadata.name is required" { - t.Error(err) - } -} - -func verifyChart(t *testing.T, c *chart.Chart) { - t.Helper() - if c.Name() == "" { - t.Fatalf("No chart metadata found on %v", c) - } - t.Logf("Verifying chart %s", c.Name()) - if len(c.Templates) != 1 { - t.Errorf("Expected 1 template, got %d", len(c.Templates)) - } - - numfiles := 6 - if len(c.Files) != numfiles { - t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) - for _, n := range c.Files { - t.Logf("\t%s", n.Name) - } - } - - if len(c.Dependencies()) != 2 { - t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies()) - for _, d := range c.Dependencies() { - t.Logf("\tSubchart: %s\n", d.Name()) - } - } - - expect := map[string]map[string]string{ - "alpine": { - "version": "0.1.0", - }, - "mariner": { - "version": "4.3.2", - }, - } - - for _, dep := range c.Dependencies() { - if dep.Metadata == nil { - t.Fatalf("expected metadata on dependency: %v", dep) - } - exp, ok := expect[dep.Name()] - if !ok { - t.Fatalf("Unknown dependency %s", dep.Name()) - } - if exp["version"] != dep.Metadata.Version { - t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) - } - } - -} - -func verifyDependencies(t *testing.T, c *chart.Chart) { - if len(c.Metadata.Dependencies) != 2 { - t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) - } - tests := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, - } - for i, tt := range tests { - d := c.Metadata.Dependencies[i] - if d.Name != tt.Name { - t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) - } - if d.Version != tt.Version { - t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) - } - if d.Repository != tt.Repository { - t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) - } - } -} - -func verifyDependenciesLock(t *testing.T, c *chart.Chart) { - if len(c.Metadata.Dependencies) != 2 { - t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) - } - tests := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, - } - for i, tt := range tests { - d := c.Metadata.Dependencies[i] - if d.Name != tt.Name { - t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) - } - if d.Version != tt.Version { - t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) - } - if d.Repository != tt.Repository { - t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) - } - } -} + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var reader io.Reader + if tc.inputReader != nil { + reader = tc.inputReader + } else { + reader = createChartArchive(t, tc.chartName, tc.apiVersion, tc.extraFiles, tc.createChartYaml) + } -func verifyFrobnitz(t *testing.T, c *chart.Chart) { - verifyChartFileAndTemplate(t, c, "frobnitz") -} + loadedChart, err := LoadArchive(context.Background(), reader) -func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { - if c.Metadata == nil { - t.Fatal("Metadata is nil") - } - if c.Name() != name { - t.Errorf("Expected %s, got %s", name, c.Name()) - } - if len(c.Templates) != 1 { - t.Fatalf("Expected 1 template, got %d", len(c.Templates)) - } - if c.Templates[0].Name != "templates/template.tpl" { - t.Errorf("Unexpected template: %s", c.Templates[0].Name) - } - if len(c.Templates[0].Data) == 0 { - t.Error("No template data.") - } - if len(c.Files) != 6 { - t.Fatalf("Expected 6 Files, got %d", len(c.Files)) - } - if len(c.Dependencies()) != 2 { - t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies())) - } - if len(c.Metadata.Dependencies) != 2 { - t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies)) - } - if len(c.Lock.Dependencies) != 2 { - t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies)) - } - - for _, dep := range c.Dependencies() { - switch dep.Name() { - case "mariner": - case "alpine": - if len(dep.Templates) != 1 { - t.Fatalf("Expected 1 template, got %d", len(dep.Templates)) - } - if dep.Templates[0].Name != "templates/alpine-pod.yaml" { - t.Errorf("Unexpected template: %s", dep.Templates[0].Name) + if tc.expectedError != "" { + if err == nil || !strings.Contains(err.Error(), tc.expectedError) { + t.Errorf("Expected error containing %q, but got %v", tc.expectedError, err) + } + return } - if len(dep.Templates[0].Data) == 0 { - t.Error("No template data.") + + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - if len(dep.Files) != 1 { - t.Fatalf("Expected 1 Files, got %d", len(dep.Files)) + lac, err := chart.NewAccessor(loadedChart) + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - if len(dep.Dependencies()) != 2 { - t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies())) + eac, err := chart.NewAccessor(tc.expectedChart) + if err != nil { + t.Fatalf("Unexpected error: %v", err) } - default: - t.Errorf("Unexpected dependency %s", dep.Name()) - } - } -} -func verifyBomStripped(t *testing.T, files []*chart.File) { - for _, file := range files { - if bytes.HasPrefix(file.Data, utf8bom) { - t.Errorf("Byte Order Mark still present in processed file %s", file.Name) - } - } -} - -func TestLoadFilesRuntimeFiles(t *testing.T) { - files := []*BufferedFile{ - { - Name: "Chart.yaml", - Data: []byte(`apiVersion: v2 -name: test-chart -version: "1.0.0" -`), - }, - { - Name: "ts/runtime.ts", - Data: []byte("console.log('runtime');"), - }, - { - Name: "ts/utils/helper.ts", - Data: []byte("export function helper() {}"), - }, - { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), - }, - { - Name: "values.yaml", - Data: []byte("key: value"), - }, - } - - c, err := LoadFiles(files, *GlobalLoadOptions) - if err != nil { - t.Fatalf("Expected files to be loaded, got %v", err) - } - - // Verify RuntimeFiles are loaded correctly - if len(c.RuntimeFiles) != 2 { - t.Errorf("Expected 2 runtime files, got %d", len(c.RuntimeFiles)) - } - - expectedRuntimeFiles := map[string][]byte{ - "ts/runtime.ts": []byte("console.log('runtime');"), - "ts/utils/helper.ts": []byte("export function helper() {}"), - } - - for _, rf := range c.RuntimeFiles { - expected, ok := expectedRuntimeFiles[rf.Name] - if !ok { - t.Errorf("Unexpected runtime file: %s", rf.Name) - continue - } - if !bytes.Equal(rf.Data, expected) { - t.Errorf("Runtime file %s has unexpected content", rf.Name) - } - } - - // Verify runtime files are NOT in Files collection - for _, f := range c.Files { - if strings.HasPrefix(f.Name, "ts/") { - t.Errorf("Runtime file %s should not be in Files collection", f.Name) - } - } - - // Verify runtime files are NOT in Templates collection - for _, f := range c.Templates { - if strings.HasPrefix(f.Name, "ts/") { - t.Errorf("Runtime file %s should not be in Templates collection", f.Name) - } - } - - // Verify other files are loaded correctly - // Note: default ChartTypeChart adds _werf_helpers.tpl template, so we expect 2 templates - if len(c.Templates) != 2 { - t.Errorf("Expected 2 templates, got %d", len(c.Templates)) - } + if lac.Name() != eac.Name() { + t.Errorf("Expected chart name %q, got %q", eac.Name(), lac.Name()) + } - // Verify the user template is present - foundDeployment := false - for _, tmpl := range c.Templates { - if tmpl.Name == "templates/deployment.yaml" { - foundDeployment = true - break - } - } - if !foundDeployment { - t.Error("Expected to find templates/deployment.yaml template") + var loadedAPIVersion string + switch lc := loadedChart.(type) { + case *c2.Chart: + loadedAPIVersion = lc.Metadata.APIVersion + case *c3.Chart: + loadedAPIVersion = lc.Metadata.APIVersion + } + if loadedAPIVersion != tc.apiVersion { + t.Errorf("Expected API version %q, got %q", tc.apiVersion, loadedAPIVersion) + } + }) } } diff --git a/pkg/helm/pkg/chart/metadata.go b/pkg/helm/pkg/chart/metadata.go deleted file mode 100644 index a08a97cd..00000000 --- a/pkg/helm/pkg/chart/metadata.go +++ /dev/null @@ -1,178 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chart - -import ( - "path/filepath" - "strings" - "unicode" - - "github.com/Masterminds/semver/v3" -) - -// Maintainer describes a Chart maintainer. -type Maintainer struct { - // Name is a user name or organization name - Name string `json:"name,omitempty"` - // Email is an optional email address to contact the named maintainer - Email string `json:"email,omitempty"` - // URL is an optional URL to an address for the named maintainer - URL string `json:"url,omitempty"` -} - -// Validate checks valid data and sanitizes string characters. -func (m *Maintainer) Validate() error { - if m == nil { - return ValidationError("maintainers must not contain empty or null nodes") - } - m.Name = sanitizeString(m.Name) - m.Email = sanitizeString(m.Email) - m.URL = sanitizeString(m.URL) - return nil -} - -// Metadata for a Chart file. This models the structure of a Chart.yaml file. -type Metadata struct { - // The name of the chart. Required. - Name string `json:"name,omitempty"` - // The URL to a relevant project page, git repo, or contact person - Home string `json:"home,omitempty"` - // Source is the URL to the source code of this chart - Sources []string `json:"sources,omitempty"` - // A SemVer 2 conformant version string of the chart. Required. - Version string `json:"version,omitempty"` - // A one-sentence description of the chart - Description string `json:"description,omitempty"` - // A list of string keywords - Keywords []string `json:"keywords,omitempty"` - // A list of name and URL/email address combinations for the maintainer(s) - Maintainers []*Maintainer `json:"maintainers,omitempty"` - // The URL to an icon file. - Icon string `json:"icon,omitempty"` - // The API Version of this chart. Required. - APIVersion string `json:"apiVersion,omitempty"` - // The condition to check to enable chart - Condition string `json:"condition,omitempty"` - // The tags to check to enable chart - Tags string `json:"tags,omitempty"` - // The version of the application enclosed inside of this chart. - AppVersion string `json:"appVersion,omitempty"` - // Whether or not this chart is deprecated - Deprecated bool `json:"deprecated,omitempty"` - // Annotations are additional mappings uninterpreted by Helm, - // made available for inspection by other applications. - Annotations map[string]string `json:"annotations,omitempty"` - // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. - KubeVersion string `json:"kubeVersion,omitempty"` - // Dependencies are a list of dependencies for a chart. - Dependencies []*Dependency `json:"dependencies,omitempty"` - // Specifies the chart type: application or library - Type string `json:"type,omitempty"` -} - -// Validate checks the metadata for known issues and sanitizes string -// characters. -func (md *Metadata) Validate() error { - if md == nil { - return ValidationError("chart.metadata is required") - } - - md.Name = sanitizeString(md.Name) - md.Description = sanitizeString(md.Description) - md.Home = sanitizeString(md.Home) - md.Icon = sanitizeString(md.Icon) - md.Condition = sanitizeString(md.Condition) - md.Tags = sanitizeString(md.Tags) - md.AppVersion = sanitizeString(md.AppVersion) - md.KubeVersion = sanitizeString(md.KubeVersion) - for i := range md.Sources { - md.Sources[i] = sanitizeString(md.Sources[i]) - } - for i := range md.Keywords { - md.Keywords[i] = sanitizeString(md.Keywords[i]) - } - - if md.APIVersion == "" { - return ValidationError("chart.metadata.apiVersion is required") - } - if md.Name == "" { - return ValidationError("chart.metadata.name is required") - } - - if md.Name != filepath.Base(md.Name) { - return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) - } - - if md.Version == "" { - return ValidationError("chart.metadata.version is required") - } - if !isValidSemver(md.Version) { - return ValidationErrorf("chart.metadata.version %q is invalid", md.Version) - } - if !isValidChartType(md.Type) { - return ValidationError("chart.metadata.type must be application or library") - } - - for _, m := range md.Maintainers { - if err := m.Validate(); err != nil { - return err - } - } - - // Aliases need to be validated here to make sure that the alias name does - // not contain any illegal characters. - dependencies := map[string]*Dependency{} - for _, dependency := range md.Dependencies { - if err := dependency.Validate(); err != nil { - return err - } - key := dependency.Name - if dependency.Alias != "" { - key = dependency.Alias - } - if dependencies[key] != nil { - return ValidationErrorf("more than one dependency with name or alias %q", key) - } - dependencies[key] = dependency - } - return nil -} - -func isValidChartType(in string) bool { - switch in { - case "", "application", "library": - return true - } - return false -} - -func isValidSemver(v string) bool { - _, err := semver.NewVersion(v) - return err == nil -} - -// sanitizeString normalize spaces and removes non-printable characters. -func sanitizeString(str string) string { - return strings.Map(func(r rune) rune { - if unicode.IsSpace(r) { - return ' ' - } - if unicode.IsPrint(r) { - return r - } - return -1 - }, str) -} diff --git a/pkg/helm/pkg/chart/metadata_test.go b/pkg/helm/pkg/chart/metadata_test.go deleted file mode 100644 index 62aea726..00000000 --- a/pkg/helm/pkg/chart/metadata_test.go +++ /dev/null @@ -1,201 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package chart - -import ( - "testing" -) - -func TestValidate(t *testing.T) { - tests := []struct { - name string - md *Metadata - err error - }{ - { - "chart without metadata", - nil, - ValidationError("chart.metadata is required"), - }, - { - "chart without apiVersion", - &Metadata{Name: "test", Version: "1.0"}, - ValidationError("chart.metadata.apiVersion is required"), - }, - { - "chart without name", - &Metadata{APIVersion: "v2", Version: "1.0"}, - ValidationError("chart.metadata.name is required"), - }, - { - "chart without name", - &Metadata{Name: "../../test", APIVersion: "v2", Version: "1.0"}, - ValidationError("chart.metadata.name \"../../test\" is invalid"), - }, - { - "chart without version", - &Metadata{Name: "test", APIVersion: "v2"}, - ValidationError("chart.metadata.version is required"), - }, - { - "chart with bad type", - &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "test"}, - ValidationError("chart.metadata.type must be application or library"), - }, - { - "chart without dependency", - &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"}, - nil, - }, - { - "dependency with valid alias", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - {Name: "dependency", Alias: "legal-alias"}, - }, - }, - nil, - }, - { - "dependency with bad characters in alias", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - {Name: "bad", Alias: "illegal alias"}, - }, - }, - ValidationError("dependency \"bad\" has disallowed characters in the alias"), - }, - { - "same dependency twice", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - {Name: "foo", Alias: ""}, - {Name: "foo", Alias: ""}, - }, - }, - ValidationError("more than one dependency with name or alias \"foo\""), - }, - { - "two dependencies with alias from second dependency shadowing first one", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - {Name: "foo", Alias: ""}, - {Name: "bar", Alias: "foo"}, - }, - }, - ValidationError("more than one dependency with name or alias \"foo\""), - }, - { - // this case would make sense and could work in future versions of Helm, currently template rendering would - // result in undefined behaviour - "same dependency twice with different version", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - {Name: "foo", Alias: "", Version: "1.2.3"}, - {Name: "foo", Alias: "", Version: "1.0.0"}, - }, - }, - ValidationError("more than one dependency with name or alias \"foo\""), - }, - { - // this case would make sense and could work in future versions of Helm, currently template rendering would - // result in undefined behaviour - "two dependencies with same name but different repos", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - {Name: "foo", Repository: "repo-0"}, - {Name: "foo", Repository: "repo-1"}, - }, - }, - ValidationError("more than one dependency with name or alias \"foo\""), - }, - { - "dependencies has nil", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Dependencies: []*Dependency{ - nil, - }, - }, - ValidationError("dependencies must not contain empty or null nodes"), - }, - { - "maintainer not empty", - &Metadata{ - Name: "test", - APIVersion: "v2", - Version: "1.0", - Type: "application", - Maintainers: []*Maintainer{ - nil, - }, - }, - ValidationError("maintainers must not contain empty or null nodes"), - }, - { - "version invalid", - &Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"}, - ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"), - }, - } - - for _, tt := range tests { - result := tt.md.Validate() - if result != tt.err { - t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name) - } - } -} - -func TestValidate_sanitize(t *testing.T) { - md := &Metadata{APIVersion: "v2", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}} - if err := md.Validate(); err != nil { - t.Fatalf("unexpected error: %s", err) - } - if md.Description != "description test" { - t.Fatalf("description was not sanitized: %q", md.Description) - } - if md.Maintainers[0].Name != " " { - t.Fatal("maintainer name was not sanitized") - } -} diff --git a/pkg/helm/pkg/chart/v2/chart.go b/pkg/helm/pkg/chart/v2/chart.go new file mode 100644 index 00000000..ab9e69d2 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/chart.go @@ -0,0 +1,221 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/samber/lo" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +// APIVersionV1 is the API version number for version 1. +const APIVersionV1 = "v1" + +// APIVersionV2 is the API version number for version 2. +const APIVersionV2 = "v2" + +// aliasNameFormat defines the characters that are legal in an alias name. +var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$") + +// Chart is a helm package that contains metadata, a default config, zero or more +// optionally parameterizable templates, and zero or more charts (dependencies). +type Chart struct { + // Raw contains the raw contents of the files originally contained in the chart archive. + // + // This should not be used except in special cases like `helm show values`, + // where we want to display the raw values, comments and all. + Raw []*common.File `json:"-"` + // Metadata is the contents of the Chartfile. + Metadata *Metadata `json:"metadata"` + // Lock is the contents of Chart.lock. + Lock *Lock `json:"lock"` + // Templates for this chart. + Templates []*common.File `json:"templates"` + // Values are default config for this chart. + Values map[string]interface{} `json:"values"` + // Schema is an optional JSON schema for imposing structure on Values + Schema []byte `json:"schema"` + // SchemaModTime the schema was last modified + SchemaModTime time.Time `json:"schemamodtime,omitempty"` + // Files are miscellaneous files in a chart archive, + // e.g. README, LICENSE, etc. + Files []*common.File `json:"files"` + // ModTime the chart metadata was last modified + ModTime time.Time `json:"modtime,omitzero"` + + RuntimeFiles []*common.File `json:"-"` + ExtraValues map[string]interface{} `json:"-"` + SecretsRuntimeData common.RuntimeData `json:"-"` + + parent *Chart + dependencies []*Chart +} + +type CRD struct { + // Name is the File.Name for the crd file + Name string + // Filename is the File obj Name including (sub-)chart.ChartFullPath + Filename string + // File is the File obj for the crd + File *common.File +} + +// SetDependencies replaces the chart dependencies. +func (ch *Chart) SetDependencies(charts ...*Chart) { + ch.dependencies = nil + ch.AddDependency(charts...) +} + +// Name returns the name of the chart. +func (ch *Chart) Name() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.Name +} + +// AddDependency determines if the chart is a subchart. +func (ch *Chart) AddDependency(charts ...*Chart) { + for i, x := range charts { + charts[i].parent = ch + ch.dependencies = append(ch.dependencies, x) + } +} + +// Root finds the root chart. +func (ch *Chart) Root() *Chart { + if ch.IsRoot() { + return ch + } + return ch.Parent().Root() +} + +// Dependencies are the charts that this chart depends on. +func (ch *Chart) Dependencies() []*Chart { return ch.dependencies } + +// IsRoot determines if the chart is the root chart. +func (ch *Chart) IsRoot() bool { return ch.parent == nil } + +// Parent returns a subchart's parent chart. +func (ch *Chart) Parent() *Chart { return ch.parent } + +// ChartPath returns the full path to this chart in dot notation. +func (ch *Chart) ChartPath() string { + if !ch.IsRoot() { + return ch.Parent().ChartPath() + "." + ch.Name() + } + return ch.Name() +} + +// ChartFullPath returns the full path to this chart. +// Note that the path may not correspond to the path where the file can be found on the file system if the path +// points to an aliased subchart. +func (ch *Chart) ChartFullPath() string { + if !ch.IsRoot() { + return ch.Parent().ChartFullPath() + "/charts/" + ch.Name() + } + return ch.Name() +} + +// Validate validates the metadata. +func (ch *Chart) Validate() error { + return ch.Metadata.Validate() +} + +// AppVersion returns the appversion of the chart. +func (ch *Chart) AppVersion() string { + if ch.Metadata == nil { + return "" + } + return ch.Metadata.AppVersion +} + +// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. +// Deprecated: use CRDObjects() +func (ch *Chart) CRDs() []*common.File { + files := []*common.File{} + // Find all resources in the crds/ directory + for _, f := range ch.Files { + if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { + files = append(files, f) + } + } + // Get CRDs from dependencies, too. + for _, dep := range ch.Dependencies() { + files = append(files, dep.CRDs()...) + } + return files +} + +// CRDObjects returns a list of CRD objects in the 'crds/' directory of a Helm chart & subcharts +func (ch *Chart) CRDObjects() []CRD { + crds := []CRD{} + // Find all resources in the crds/ directory + for _, f := range ch.Files { + if strings.HasPrefix(f.Name, "crds/") && hasManifestExtension(f.Name) { + mycrd := CRD{Name: f.Name, Filename: filepath.Join(ch.ChartFullPath(), f.Name), File: f} + crds = append(crds, mycrd) + } + } + // Get CRDs from dependencies, too. + for _, dep := range ch.Dependencies() { + crds = append(crds, dep.CRDObjects()...) + } + return crds +} + +func hasManifestExtension(fname string) bool { + ext := filepath.Ext(fname) + return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") || strings.EqualFold(ext, ".json") +} + +func (ch *Chart) AddRuntimeFile(name string, data []byte) { + ch.Raw = append(ch.Raw, &common.File{Name: name, Data: data}) + + ch.RuntimeFiles = append(ch.RuntimeFiles, &common.File{Name: name, Data: data}) + if !ch.IsRoot() { + root := ch.Root() + rawName := getRootRawFileName(ch, name) + root.Raw = append(root.Raw, &common.File{Name: rawName, Data: data}) + } +} + +func (ch *Chart) RemoveRuntimeFile(name string) { + ch.Raw = lo.Reject(ch.Raw, func(f *common.File, _ int) bool { + return f.Name == name + }) + + ch.RuntimeFiles = lo.Reject(ch.RuntimeFiles, func(f *common.File, _ int) bool { + return f.Name == name + }) + + if !ch.IsRoot() { + root := ch.Root() + rawName := getRootRawFileName(ch, name) + root.Raw = lo.Reject(root.Raw, func(f *common.File, _ int) bool { + return f.Name == rawName + }) + } +} + +func getRootRawFileName(ch *Chart, name string) string { + return filepath.Join(strings.TrimPrefix(ch.ChartFullPath(), ch.Root().Name()+"/"), name) +} diff --git a/pkg/helm/pkg/chart/v2/chart_test.go b/pkg/helm/pkg/chart/v2/chart_test.go new file mode 100644 index 00000000..bdd11b17 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/chart_test.go @@ -0,0 +1,229 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v2 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +func TestCRDs(t *testing.T) { + modTime := time.Now() + chrt := Chart{ + Files: []*common.File{ + { + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDs() + is.Equal(2, len(crds)) + is.Equal("crds/foo.yaml", crds[0].Name) + is.Equal("crds/foo/bar/baz.yaml", crds[1].Name) +} + +func TestSaveChartNoRawData(t *testing.T) { + chrt := Chart{ + Raw: []*common.File{ + { + Name: "fhqwhgads.yaml", + ModTime: time.Now(), + Data: []byte("Everybody to the Limit"), + }, + }, + } + + is := assert.New(t) + data, err := json.Marshal(chrt) + if err != nil { + t.Fatal(err) + } + + res := &Chart{} + if err := json.Unmarshal(data, res); err != nil { + t.Fatal(err) + } + + is.Equal([]*common.File(nil), res.Raw) +} + +func TestMetadata(t *testing.T) { + chrt := Chart{ + Metadata: &Metadata{ + Name: "foo.yaml", + AppVersion: "1.0.0", + APIVersion: "v2", + Version: "1.0.0", + Type: "application", + }, + } + + is := assert.New(t) + + is.Equal("foo.yaml", chrt.Name()) + is.Equal("1.0.0", chrt.AppVersion()) + is.Equal(nil, chrt.Validate()) +} + +func TestIsRoot(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal(false, chrt1.IsRoot()) + is.Equal(true, chrt2.IsRoot()) +} + +func TestChartPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo.", chrt1.ChartPath()) + is.Equal("foo", chrt2.ChartPath()) +} + +func TestChartFullPath(t *testing.T) { + chrt1 := Chart{ + parent: &Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + }, + } + + chrt2 := Chart{ + Metadata: &Metadata{ + Name: "foo", + }, + } + + is := assert.New(t) + + is.Equal("foo/charts/", chrt1.ChartFullPath()) + is.Equal("foo", chrt2.ChartFullPath()) +} + +func TestCRDObjects(t *testing.T) { + modTime := time.Now() + chrt := Chart{ + Files: []*common.File{ + { + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + { + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), + }, + }, + } + + expected := []CRD{ + { + Name: "crds/foo.yaml", + Filename: "crds/foo.yaml", + File: &common.File{ + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + }, + { + Name: "crds/foo/bar/baz.yaml", + Filename: "crds/foo/bar/baz.yaml", + File: &common.File{ + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), + }, + }, + } + + is := assert.New(t) + crds := chrt.CRDObjects() + is.Equal(expected, crds) +} diff --git a/pkg/helm/pkg/chart/v2/dependency.go b/pkg/helm/pkg/chart/v2/dependency.go new file mode 100644 index 00000000..1fb4534d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/dependency.go @@ -0,0 +1,83 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import "time" + +// Dependency describes a chart upon which another chart depends. +// +// Dependencies can be used to express developer intent, or to capture the state +// of a chart. +type Dependency struct { + // Name is the name of the dependency. + // + // This must mach the name in the dependency's Chart.yaml. + Name string `json:"name" yaml:"name"` + // Version is the version (range) of this chart. + // + // A lock file will always produce a single version, while a dependency + // may contain a semantic version range. + Version string `json:"version,omitempty" yaml:"version,omitempty"` + // The URL to the repository. + // + // Appending `index.yaml` to this string should result in a URL that can be + // used to fetch the repository index. + Repository string `json:"repository" yaml:"repository"` + // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) + Condition string `json:"condition,omitempty" yaml:"condition,omitempty"` + // Tags can be used to group charts for enabling/disabling together + Tags []string `json:"tags,omitempty" yaml:"tags,omitempty"` + // Enabled bool determines if chart should be loaded + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a + // string or pair of child/parent sublist items. + ImportValues []interface{} `json:"import-values,omitempty" yaml:"import-values,omitempty"` + // Alias usable alias to be used for the chart + Alias string `json:"alias,omitempty" yaml:"alias,omitempty"` + ExportValues []interface{} `json:"export-values,omitempty" yaml:"export-values,omitempty"` +} + +// Validate checks for common problems with the dependency datastructure in +// the chart. This check must be done at load time before the dependency's charts are +// loaded. +func (d *Dependency) Validate() error { + if d == nil { + return ValidationError("dependencies must not contain empty or null nodes") + } + d.Name = sanitizeString(d.Name) + d.Version = sanitizeString(d.Version) + d.Repository = sanitizeString(d.Repository) + d.Condition = sanitizeString(d.Condition) + for i := range d.Tags { + d.Tags[i] = sanitizeString(d.Tags[i]) + } + if d.Alias != "" && !aliasNameFormat.MatchString(d.Alias) { + return ValidationErrorf("dependency %q has disallowed characters in the alias", d.Name) + } + return nil +} + +// Lock is a lock file for dependencies. +// +// It represents the state that the dependencies should be in. +type Lock struct { + // Generated is the date the lock file was last generated. + Generated time.Time `json:"generated"` + // Digest is a hash of the dependencies in Chart.yaml. + Digest string `json:"digest"` + // Dependencies is the list of dependencies that this lock file has locked. + Dependencies []*Dependency `json:"dependencies"` +} diff --git a/pkg/helm/pkg/chart/v2/dependency_test.go b/pkg/helm/pkg/chart/v2/dependency_test.go new file mode 100644 index 00000000..35919bd7 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/dependency_test.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v2 + +import ( + "testing" +) + +func TestValidateDependency(t *testing.T) { + dep := &Dependency{ + Name: "example", + } + for value, shouldFail := range map[string]bool{ + "abcdefghijklmenopQRSTUVWXYZ-0123456780_": false, + "-okay": false, + "_okay": false, + "- bad": true, + " bad": true, + "bad\nvalue": true, + "bad ": true, + "bad$": true, + } { + dep.Alias = value + res := dep.Validate() + if res != nil && !shouldFail { + t.Errorf("Failed on case %q", dep.Alias) + } else if res == nil && shouldFail { + t.Errorf("Expected failure for %q", dep.Alias) + } + } +} diff --git a/pkg/helm/pkg/chart/v2/doc.go b/pkg/helm/pkg/chart/v2/doc.go new file mode 100644 index 00000000..d36ca3ec --- /dev/null +++ b/pkg/helm/pkg/chart/v2/doc.go @@ -0,0 +1,23 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package v2 provides chart handling for apiVersion v1 and v2 charts + +This package and its sub-packages provide handling for apiVersion v1 and v2 charts. +The changes from v1 to v2 charts are minor and were able to be handled with minor +switches based on characteristics. +*/ +package v2 diff --git a/pkg/helm/pkg/chart/v2/errors.go b/pkg/helm/pkg/chart/v2/errors.go new file mode 100644 index 00000000..eeef7531 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/errors.go @@ -0,0 +1,30 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import "fmt" + +// ValidationError represents a data validation error. +type ValidationError string + +func (v ValidationError) Error() string { + return "validation: " + string(v) +} + +// ValidationErrorf takes a message and formatting options and creates a ValidationError +func ValidationErrorf(msg string, args ...interface{}) ValidationError { + return ValidationError(fmt.Sprintf(msg, args...)) +} diff --git a/pkg/helm/pkg/chart/v2/fuzz_test.go b/pkg/helm/pkg/chart/v2/fuzz_test.go new file mode 100644 index 00000000..a897ef7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/fuzz_test.go @@ -0,0 +1,48 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "testing" + + fuzz "github.com/AdaLogics/go-fuzz-headers" +) + +func FuzzMetadataValidate(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + fdp := fuzz.NewConsumer(data) + // Add random values to the metadata + md := &Metadata{} + err := fdp.GenerateStruct(md) + if err != nil { + t.Skip() + } + md.Validate() + }) +} + +func FuzzDependencyValidate(f *testing.F) { + f.Fuzz(func(t *testing.T, data []byte) { + f := fuzz.NewConsumer(data) + // Add random values to the dependenci + d := &Dependency{} + err := f.GenerateStruct(d) + if err != nil { + t.Skip() + } + d.Validate() + }) +} diff --git a/pkg/helm/pkg/chart/v2/lint/lint.go b/pkg/helm/pkg/chart/v2/lint/lint.go new file mode 100644 index 00000000..2e028a13 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/lint.go @@ -0,0 +1,71 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint // import "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint" + +import ( + "path/filepath" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/rules" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" +) + +type linterOptions struct { + KubeVersion *common.KubeVersion + SkipSchemaValidation bool +} + +type LinterOption func(lo *linterOptions) + +func WithKubeVersion(kubeVersion *common.KubeVersion) LinterOption { + return func(lo *linterOptions) { + lo.KubeVersion = kubeVersion + } +} + +func WithSkipSchemaValidation(skipSchemaValidation bool) LinterOption { + return func(lo *linterOptions) { + lo.SkipSchemaValidation = skipSchemaValidation + } +} + +func RunAll(baseDir string, values map[string]interface{}, namespace string, options ...LinterOption) support.Linter { + + chartDir, _ := filepath.Abs(baseDir) + + lo := linterOptions{} + for _, option := range options { + option(&lo) + } + + result := support.Linter{ + ChartDir: chartDir, + } + + rules.Chartfile(&result) + rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation) + rules.Templates( + &result, + namespace, + values, + rules.TemplateLinterKubeVersion(lo.KubeVersion), + rules.TemplateLinterSkipSchemaValidation(lo.SkipSchemaValidation)) + rules.Dependencies(&result) + rules.Crds(&result) + + return result +} diff --git a/pkg/helm/pkg/chart/v2/lint/lint_test.go b/pkg/helm/pkg/chart/v2/lint/lint_test.go new file mode 100644 index 00000000..0e136a99 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/lint_test.go @@ -0,0 +1,247 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package lint + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" +) + +const namespace = "testNamespace" + +const badChartDir = "rules/testdata/badchartfile" +const badValuesFileDir = "rules/testdata/badvaluesfile" +const badYamlFileDir = "rules/testdata/albatross" +const badCrdFileDir = "rules/testdata/badcrdfile" +const goodChartDir = "rules/testdata/goodone" +const subChartValuesDir = "rules/testdata/withsubchart" +const malformedTemplate = "rules/testdata/malformed-template" +const invalidChartFileDir = "rules/testdata/invalidchartfile" + +func TestBadChart(t *testing.T) { + var values map[string]any + m := RunAll(badChartDir, values, namespace).Messages + if len(m) != 9 { + t.Errorf("Number of errors %v", len(m)) + t.Errorf("All didn't fail with expected errors, got %#v", m) + } + // There should be one INFO, 2 WARNING and 2 ERROR messages, check for them + var i, w, w2, e, e2, e3, e4, e5, e6 bool + for _, msg := range m { + if msg.Severity == support.InfoSev { + if strings.Contains(msg.Err.Error(), "icon is recommended") { + i = true + } + } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "does not exist") { + w = true + } + } + if msg.Severity == support.ErrorSev { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + e = true + } + if strings.Contains(msg.Err.Error(), "name is required") { + e2 = true + } + + if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { + e3 = true + } + + if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { + e4 = true + } + + if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + e5 = true + } + // This comes from the dependency check, which loads dependency info from the Chart.yaml + if strings.Contains(msg.Err.Error(), "unable to load chart") { + e6 = true + } + } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") { + w2 = true + } + } + } + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w || !w2 { + t.Errorf("Didn't find all the expected errors, got %#v", m) + } +} + +func TestInvalidYaml(t *testing.T) { + var values map[string]any + m := RunAll(badYamlFileDir, values, namespace).Messages + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("All didn't have the error for deliberateSyntaxError") + } +} + +func TestInvalidChartYaml(t *testing.T) { + var values map[string]any + m := RunAll(invalidChartFileDir, values, namespace).Messages + if len(m) != 2 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "failed to strictly parse chart metadata file") { + t.Errorf("All didn't have the error for duplicate YAML keys") + } +} + +func TestBadValues(t *testing.T) { + var values map[string]any + m := RunAll(badValuesFileDir, values, namespace).Messages + if len(m) < 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { + t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) + } +} + +func TestBadCrdFile(t *testing.T) { + var values map[string]any + m := RunAll(badCrdFileDir, values, namespace).Messages + assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) + assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") + assert.ErrorContains(t, m[1].Err, "object kind is not 'CustomResourceDefinition'") +} + +func TestGoodChart(t *testing.T) { + var values map[string]any + m := RunAll(goodChartDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. +// +// See https://github.com/helm/helm/issues/7923 +func TestHelmCreateChart(t *testing.T) { + var values map[string]any + dir := t.TempDir() + + createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) + if err != nil { + t.Error(err) + // Fatal is bad because of the defer. + return + } + + // Note: we test with strict=true here, even though others have + // strict = false. + m := RunAll(createdChart, values, namespace, WithSkipSchemaValidation(true)).Messages + if ll := len(m); ll != 1 { + t.Errorf("All should have had exactly 1 error. Got %d", ll) + for i, msg := range m { + t.Logf("Message %d: %s", i, msg.Error()) + } + } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { + t.Errorf("Unexpected lint error: %s", msg) + } +} + +// TestHelmCreateChart_CheckDeprecatedWarnings checks if any default template created by `helm create` throws +// deprecated warnings in the linter check against the current Kubernetes version (provided using ldflags). +// +// See https://github.com/helm/helm/issues/11495 +// +// Resources like hpa and ingress, which are disabled by default in values.yaml are enabled here using the equivalent +// of the `--set` flag. +func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { + createdChart, err := chartutil.Create("checkdeprecatedwarnings", t.TempDir()) + if err != nil { + t.Error(err) + return + } + + // Add values to enable hpa, and ingress which are disabled by default. + // This is the equivalent of: + // helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true' + updatedValues := map[string]any{ + "autoscaling": map[string]any{ + "enabled": true, + }, + "ingress": map[string]any{ + "enabled": true, + }, + } + + linterRunDetails := RunAll(createdChart, updatedValues, namespace, WithSkipSchemaValidation(true)) + for _, msg := range linterRunDetails.Messages { + if strings.HasPrefix(msg.Error(), "[WARNING]") && + strings.Contains(msg.Error(), "deprecated") { + // When there is a deprecation warning for an object created + // by `helm create` for the current Kubernetes version, fail. + t.Errorf("Unexpected deprecation warning for %q: %s", msg.Path, msg.Error()) + } + } +} + +// lint ignores import-values +// See https://github.com/helm/helm/issues/9658 +func TestSubChartValuesChart(t *testing.T) { + var values map[string]any + m := RunAll(subChartValuesDir, values, namespace).Messages + if len(m) != 0 { + t.Error("All returned linter messages when it shouldn't have") + for i, msg := range m { + t.Logf("Message %d: %s", i, msg) + } + } +} + +// lint stuck with malformed template object +// See https://github.com/helm/helm/issues/11391 +func TestMalformedTemplate(t *testing.T) { + var values map[string]any + c := time.After(3 * time.Second) + ch := make(chan int, 1) + var m []support.Message + go func() { + m = RunAll(malformedTemplate, values, namespace).Messages + ch <- 1 + }() + select { + case <-c: + t.Fatalf("lint malformed template timeout") + case <-ch: + if len(m) != 1 { + t.Fatalf("All didn't fail with expected errors, got %#v", m) + } + if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { + t.Errorf("All didn't have the error for invalid character '{'") + } + } +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/chartfile.go b/pkg/helm/pkg/chart/v2/lint/rules/chartfile.go new file mode 100644 index 00000000..a4ee83e0 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/chartfile.go @@ -0,0 +1,236 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/rules" + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/Masterminds/semver/v3" + "github.com/asaskevich/govalidator" + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" +) + +// Chartfile runs a set of linter rules related to Chart.yaml file +func Chartfile(linter *support.Linter) { + chartFileName := "Chart.yaml" + chartPath := filepath.Join(linter.ChartDir, chartFileName) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) + + chartFile, err := chartutil.LoadChartfile(chartPath) + validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) + + // Guard clause. Following linter rules require a parsable ChartFile + if !validChartFile { + return + } + + _, err = chartutil.StrictLoadChartfile(chartPath) + linter.RunLinterRule(support.WarningSev, chartFileName, validateChartYamlStrictFormat(err)) + + // type check for Chart.yaml . ignoring error as any parse + // errors would already be caught in the above load function + chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) + + // Chart metadata + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) + + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) + linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) + linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) + linter.RunLinterRule(support.WarningSev, chartFileName, validateChartVersionStrictSemVerV2(chartFile)) +} + +func validateChartVersionType(data map[string]interface{}) error { + return isStringValue(data, "version") +} + +func validateChartAppVersionType(data map[string]interface{}) error { + return isStringValue(data, "appVersion") +} + +func isStringValue(data map[string]interface{}, key string) error { + value, ok := data[key] + if !ok { + return nil + } + valueType := fmt.Sprintf("%T", value) + if valueType != "string" { + return fmt.Errorf("%s should be of type string but it's of type %s", key, valueType) + } + return nil +} + +func validateChartYamlNotDirectory(chartPath string) error { + fi, err := os.Stat(chartPath) + + if err == nil && fi.IsDir() { + return errors.New("should be a file, not a directory") + } + return nil +} + +func validateChartYamlFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("unable to parse YAML\n\t%w", chartFileError) + } + return nil +} + +func validateChartYamlStrictFormat(chartFileError error) error { + if chartFileError != nil { + return fmt.Errorf("failed to strictly parse chart metadata file\n\t%w", chartFileError) + } + return nil +} + +func validateChartName(cf *chart.Metadata) error { + if cf.Name == "" { + return errors.New("name is required") + } + name := filepath.Base(cf.Name) + if name != cf.Name { + return fmt.Errorf("chart name %q is invalid", cf.Name) + } + return nil +} + +func validateChartAPIVersion(cf *chart.Metadata) error { + if cf.APIVersion == "" { + return errors.New("apiVersion is required. The value must be either \"v1\" or \"v2\"") + } + + if cf.APIVersion != chart.APIVersionV1 && cf.APIVersion != chart.APIVersionV2 { + return fmt.Errorf("apiVersion '%s' is not valid. The value must be either \"v1\" or \"v2\"", cf.APIVersion) + } + + return nil +} + +func validateChartVersion(cf *chart.Metadata) error { + if cf.Version == "" { + return errors.New("version is required") + } + + version, err := semver.NewVersion(cf.Version) + if err != nil { + return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version) + } + + c, err := semver.NewConstraint(">0.0.0-0") + if err != nil { + return err + } + valid, msg := c.Validate(version) + + if !valid && len(msg) > 0 { + return fmt.Errorf("version %v", msg[0]) + } + + return nil +} + +func validateChartVersionStrictSemVerV2(cf *chart.Metadata) error { + _, err := semver.StrictNewVersion(cf.Version) + + if err != nil { + return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version) + } + + return nil +} + +func validateChartMaintainer(cf *chart.Metadata) error { + for _, maintainer := range cf.Maintainers { + if maintainer == nil { + return errors.New("a maintainer entry is empty") + } + if maintainer.Name == "" { + return errors.New("each maintainer requires a name") + } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { + return fmt.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) + } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { + return fmt.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) + } + } + return nil +} + +func validateChartSources(cf *chart.Metadata) error { + for _, source := range cf.Sources { + if source == "" || !govalidator.IsRequestURL(source) { + return fmt.Errorf("invalid source URL '%s'", source) + } + } + return nil +} + +func validateChartIconPresence(cf *chart.Metadata) error { + if cf.Icon == "" { + return errors.New("icon is recommended") + } + return nil +} + +func validateChartIconURL(cf *chart.Metadata) error { + if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { + return fmt.Errorf("invalid icon URL '%s'", cf.Icon) + } + return nil +} + +func validateChartDependencies(cf *chart.Metadata) error { + if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV2 { + return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) + } + return nil +} + +func validateChartType(cf *chart.Metadata) error { + if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV2 { + return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) + } + return nil +} + +// loadChartFileForTypeCheck loads the Chart.yaml +// in a generic form of a map[string]interface{}, so that the type +// of the values can be checked +func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := make(map[string]interface{}) + err = yaml.Unmarshal(b, &y) + return y, err +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/chartfile_test.go b/pkg/helm/pkg/chart/v2/lint/rules/chartfile_test.go new file mode 100644 index 00000000..c1752a9c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/chartfile_test.go @@ -0,0 +1,319 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" +) + +const ( + badChartNameDir = "testdata/badchartname" + badChartDir = "testdata/badchartfile" + anotherBadChartDir = "testdata/anotherbadchartfile" +) + +var ( + badChartNamePath = filepath.Join(badChartNameDir, "Chart.yaml") + badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") + nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") +) + +var badChart, _ = chartutil.LoadChartfile(badChartFilePath) +var badChartName, _ = chartutil.LoadChartfile(badChartNamePath) + +// Validation functions Test +func TestValidateChartYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) + defer os.Remove(nonExistingChartFilePath) + + err := validateChartYamlNotDirectory(nonExistingChartFilePath) + if err == nil { + t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error") + } +} + +func TestValidateChartYamlFormat(t *testing.T) { + err := validateChartYamlFormat(errors.New("Read error")) + if err == nil { + t.Errorf("validateChartYamlFormat to return a linter error, got no error") + } + + err = validateChartYamlFormat(nil) + if err != nil { + t.Errorf("validateChartYamlFormat to return no error, got a linter error") + } +} + +func TestValidateChartName(t *testing.T) { + err := validateChartName(badChart) + if err == nil { + t.Errorf("validateChartName to return a linter error, got no error") + } + + err = validateChartName(badChartName) + if err == nil { + t.Error("expected validateChartName to return a linter error for an invalid name, got no error") + } +} + +func TestValidateChartVersion(t *testing.T) { + var failTest = []struct { + Version string + ErrorMsg string + }{ + {"", "version is required"}, + {"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"}, + {"waps", "'waps' is not a valid SemVer"}, + {"-3", "'-3' is not a valid SemVer"}, + } + + var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} + + for _, test := range failTest { + badChart.Version = test.Version + err := validateChartVersion(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + } + } + + for _, version := range successTest { + badChart.Version = version + err := validateChartVersion(badChart) + if err != nil { + t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) + } + } +} + +func TestValidateChartVersionStrictSemVerV2(t *testing.T) { + var failTest = []struct { + Version string + ErrorMsg string + }{ + {"", "version '' is not a valid SemVerV2"}, + {"1", "version '1' is not a valid SemVerV2"}, + {"1.1", "version '1.1' is not a valid SemVerV2"}, + } + + var successTest = []string{"1.1.1", "0.0.1+build", "0.0.1-beta"} + + for _, test := range failTest { + badChart.Version = test.Version + err := validateChartVersionStrictSemVerV2(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartVersionStrictSemVerV2(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + } + } + + for _, version := range successTest { + badChart.Version = version + err := validateChartVersionStrictSemVerV2(badChart) + if err != nil { + t.Errorf("validateChartVersionStrictSemVerV2(%s) to return no error, got a linter error", version) + } + } +} + +func TestValidateChartMaintainer(t *testing.T) { + var failTest = []struct { + Name string + Email string + ErrorMsg string + }{ + {"", "", "each maintainer requires a name"}, + {"", "test@test.com", "each maintainer requires a name"}, + {"John Snow", "wrongFormatEmail.com", "invalid email"}, + } + + var successTest = []struct { + Name string + Email string + }{ + {"John Snow", ""}, + {"John Snow", "john@winterfell.com"}, + } + + for _, test := range failTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg) + } + } + + for _, test := range successTest { + badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} + err := validateChartMaintainer(badChart) + if err != nil { + t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error()) + } + } + + // Testing for an empty maintainer + badChart.Maintainers = []*chart.Maintainer{nil} + err := validateChartMaintainer(badChart) + if err == nil { + t.Errorf("validateChartMaintainer did not return error for nil maintainer as expected") + } + if err.Error() != "a maintainer entry is empty" { + t.Errorf("validateChartMaintainer returned unexpected error for nil maintainer: %s", err.Error()) + } +} + +func TestValidateChartSources(t *testing.T) { + var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"} + for _, test := range failTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid source URL") { + t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Sources = []string{test} + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestValidateChartIconPresence(t *testing.T) { + t.Run("Icon absent", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "", + } + + err := validateChartIconPresence(testChart) + + if err == nil { + t.Errorf("validateChartIconPresence to return a linter error, got no error") + } else if !strings.Contains(err.Error(), "icon is recommended") { + t.Errorf("expected %q, got %q", "icon is recommended", err.Error()) + } + }) + t.Run("Icon present", func(t *testing.T) { + testChart := &chart.Metadata{ + Icon: "http://example.org/icon.png", + } + + err := validateChartIconPresence(testChart) + + if err != nil { + t.Errorf("Unexpected error: %q", err.Error()) + } + }) +} + +func TestValidateChartIconURL(t *testing.T) { + var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"} + var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"} + for _, test := range failTest { + badChart.Icon = test + err := validateChartIconURL(badChart) + if err == nil || !strings.Contains(err.Error(), "invalid icon URL") { + t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test) + } + } + + for _, test := range successTest { + badChart.Icon = test + err := validateChartSources(badChart) + if err != nil { + t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error()) + } + } +} + +func TestChartfile(t *testing.T) { + t.Run("Chart.yaml basic validity issues", func(t *testing.T) { + linter := support.Linter{ChartDir: badChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 7 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "name is required") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + + if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } + + if !strings.Contains(msgs[4].Err.Error(), "chart type is not valid in apiVersion") { + t.Errorf("Unexpected message 4: %s", msgs[4].Err) + } + + if !strings.Contains(msgs[5].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { + t.Errorf("Unexpected message 5: %s", msgs[5].Err) + } + if !strings.Contains(msgs[6].Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") { + t.Errorf("Unexpected message 6: %s", msgs[6].Err) + } + }) + + t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { + linter := support.Linter{ChartDir: anotherBadChartDir} + Chartfile(&linter) + msgs := linter.Messages + expectedNumberOfErrorMessages := 4 + + if len(msgs) != expectedNumberOfErrorMessages { + t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) + return + } + + if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") { + t.Errorf("Unexpected message 0: %s", msgs[0].Err) + } + + if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") { + t.Errorf("Unexpected message 1: %s", msgs[1].Err) + } + + if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { + t.Errorf("Unexpected message 2: %s", msgs[2].Err) + } + if !strings.Contains(msgs[3].Err.Error(), "version '7.2445e+06' is not a valid SemVerV2") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } + }) +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/crds.go b/pkg/helm/pkg/chart/v2/lint/rules/crds.go new file mode 100644 index 00000000..2c1c327f --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/crds.go @@ -0,0 +1,116 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" +) + +// Crds lints the CRDs in the Linter. +func Crds(linter *support.Linter) { + fpath := "crds/" + crdsPath := filepath.Join(linter.ChartDir, fpath) + + // crds directory is optional + if _, err := os.Stat(crdsPath); errors.Is(err, fs.ErrNotExist) { + return + } + + crdsDirValid := linter.RunLinterRule(support.ErrorSev, fpath, validateCrdsDir(crdsPath)) + if !crdsDirValid { + return + } + + // Load chart and parse CRDs + chart, err := loader.Load(context.Background(), linter.ChartDir) + + chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + + if !chartLoaded { + return + } + + /* Iterate over all the CRDs to check: + 1. It is a YAML file and not a template + 2. The API version is apiextensions.k8s.io + 3. The kind is CustomResourceDefinition + */ + for _, crd := range chart.CRDObjects() { + fileName := crd.Name + fpath = fileName + + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(crd.File.Data), 4096) + for { + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if errors.Is(err, io.EOF) { + break + } + + // If YAML parsing fails here, it will always fail in the next block as well, so we should return here. + // This also confirms the YAML is not a template, since templates can't be decoded into a K8sYamlStruct. + if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + return + } + + if yamlStruct != nil { + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdAPIVersion(yamlStruct)) + linter.RunLinterRule(support.ErrorSev, fpath, validateCrdKind(yamlStruct)) + } + } + } +} + +// Validation functions +func validateCrdsDir(crdsPath string) error { + fi, err := os.Stat(crdsPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateCrdAPIVersion(obj *k8sYamlStruct) error { + if !strings.HasPrefix(obj.APIVersion, "apiextensions.k8s.io") { + return fmt.Errorf("apiVersion is not in 'apiextensions.k8s.io'") + } + return nil +} + +func validateCrdKind(obj *k8sYamlStruct) error { + if obj.Kind != "CustomResourceDefinition" { + return fmt.Errorf("object kind is not 'CustomResourceDefinition'") + } + return nil +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/crds_test.go b/pkg/helm/pkg/chart/v2/lint/rules/crds_test.go new file mode 100644 index 00000000..3c24922e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/crds_test.go @@ -0,0 +1,66 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" +) + +const invalidCrdsDir = "./testdata/invalidcrdsdir" + +func TestInvalidCrdsDir(t *testing.T) { + linter := support.Linter{ChartDir: invalidCrdsDir} + Crds(&linter) + res := linter.Messages + + assert.Len(t, res, 1) + assert.ErrorContains(t, res[0].Err, "not a directory") +} + +// multi-document YAML with empty documents would panic +func TestCrdWithEmptyDocument(t *testing.T) { + chartDir := t.TempDir() + + os.WriteFile(filepath.Join(chartDir, "Chart.yaml"), []byte( + `apiVersion: v1 +name: test +version: 0.1.0 +`), 0644) + + // CRD with comments before --- (creates empty document) + crdsDir := filepath.Join(chartDir, "crds") + os.Mkdir(crdsDir, 0755) + os.WriteFile(filepath.Join(crdsDir, "test.yaml"), []byte( + `# Comments create empty document +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: test.example.io +`), 0644) + + linter := support.Linter{ChartDir: chartDir} + Crds(&linter) + + assert.Len(t, linter.Messages, 0) +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/dependencies.go b/pkg/helm/pkg/chart/v2/lint/rules/dependencies.go new file mode 100644 index 00000000..293bccdc --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/dependencies.go @@ -0,0 +1,102 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/rules" + +import ( + "context" + "fmt" + "strings" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" +) + +// Dependencies runs lints against a chart's dependencies +// +// See https://github.com/helm/helm/issues/7910 +func Dependencies(linter *support.Linter) { + c, err := loader.LoadDir(context.Background(), linter.ChartDir) + if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) { + return + } + + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c)) + linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c)) + linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c)) +} + +func validateChartFormat(chartError error) error { + if chartError != nil { + return fmt.Errorf("unable to load chart\n\t%w", chartError) + } + return nil +} + +func validateDependencyInChartsDir(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Dependencies() { + dependencies[dep.Metadata.Name] = struct{}{} + } + for _, dep := range c.Metadata.Dependencies { + if _, ok := dependencies[dep.Name]; !ok { + missing = append(missing, dep.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependencyInMetadata(c *chart.Chart) (err error) { + dependencies := map[string]struct{}{} + missing := []string{} + for _, dep := range c.Metadata.Dependencies { + dependencies[dep.Name] = struct{}{} + } + for _, dep := range c.Dependencies() { + if _, ok := dependencies[dep.Metadata.Name]; !ok { + missing = append(missing, dep.Metadata.Name) + } + } + if len(missing) > 0 { + err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ",")) + } + return err +} + +func validateDependenciesUnique(c *chart.Chart) (err error) { + dependencies := map[string]*chart.Dependency{} + shadowing := []string{} + + for _, dep := range c.Metadata.Dependencies { + key := dep.Name + if dep.Alias != "" { + key = dep.Alias + } + if dependencies[key] != nil { + shadowing = append(shadowing, key) + } + dependencies[key] = dep + } + if len(shadowing) > 0 { + err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ",")) + } + return err +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/dependencies_test.go b/pkg/helm/pkg/chart/v2/lint/rules/dependencies_test.go new file mode 100644 index 00000000..4122e7ce --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/dependencies_test.go @@ -0,0 +1,157 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package rules + +import ( + "path/filepath" + "testing" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" +) + +func chartWithBadDependencies() chart.Chart { + badChartDeps := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "sub2", + }, + { + Name: "sub3", + }, + }, + }, + } + + badChartDeps.SetDependencies( + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub1", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "sub2", + Version: "0.1.0", + APIVersion: "v2", + }, + }, + ) + return badChartDeps +} + +func TestValidateDependencyInChartsDir(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInChartsDir(&c); err == nil { + t.Error("chart should have been flagged for missing deps in chart directory") + } +} + +func TestValidateDependencyInMetadata(t *testing.T) { + c := chartWithBadDependencies() + + if err := validateDependencyInMetadata(&c); err == nil { + t.Errorf("chart should have been flagged for missing deps in chart metadata") + } +} + +func TestValidateDependenciesUnique(t *testing.T) { + tests := []struct { + chart chart.Chart + }{ + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + }, + { + Name: "foo", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "bar", + }, + { + Name: "bar", + }, + }, + }, + }}, + {chart.Chart{ + Metadata: &chart.Metadata{ + Name: "badchart", + Version: "0.1.0", + APIVersion: "v2", + Dependencies: []*chart.Dependency{ + { + Name: "foo", + Alias: "baz", + }, + { + Name: "bar", + Alias: "baz", + }, + }, + }, + }}, + } + + for _, tt := range tests { + if err := validateDependenciesUnique(&tt.chart); err == nil { + t.Errorf("chart should have been flagged for dependency shadowing") + } + } +} + +func TestDependencies(t *testing.T) { + tmp := t.TempDir() + + c := chartWithBadDependencies() + err := chartutil.SaveDir(&c, tmp) + if err != nil { + t.Fatal(err) + } + linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)} + + Dependencies(&linter) + if l := len(linter.Messages); l != 2 { + t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l) + for i, msg := range linter.Messages { + t.Logf("Message: %d, Error: %#v", i, msg) + } + } +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/deprecations.go b/pkg/helm/pkg/chart/v2/lint/rules/deprecations.go new file mode 100644 index 00000000..6a4c3618 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/deprecations.go @@ -0,0 +1,94 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/rules" + +import ( + "fmt" + "strconv" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apiserver/pkg/endpoints/deprecation" + kscheme "k8s.io/client-go/kubernetes/scheme" +) + +// deprecatedAPIError indicates than an API is deprecated in Kubernetes +type deprecatedAPIError struct { + Deprecated string + Message string +} + +func (e deprecatedAPIError) Error() string { + msg := e.Message + return msg +} + +func validateNoDeprecations(resource *k8sYamlStruct, kubeVersion *common.KubeVersion) error { + // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation + if resource.APIVersion == "" { + return nil + } + if resource.Kind == "" { + return nil + } + + if kubeVersion == nil { + kubeVersion = &common.DefaultCapabilities.KubeVersion + } + + runtimeObject, err := resourceToRuntimeObject(resource) + if err != nil { + // do not error for non-kubernetes resources + if runtime.IsNotRegisteredError(err) { + return nil + } + return err + } + + kubeVersionMajor, err := strconv.Atoi(kubeVersion.Major) + if err != nil { + return err + } + kubeVersionMinor, err := strconv.Atoi(kubeVersion.Minor) + if err != nil { + return err + } + + if !deprecation.IsDeprecated(runtimeObject, kubeVersionMajor, kubeVersionMinor) { + return nil + } + gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) + return deprecatedAPIError{ + Deprecated: gvk, + Message: deprecation.WarningMessage(runtimeObject), + } +} + +func resourceToRuntimeObject(resource *k8sYamlStruct) (runtime.Object, error) { + scheme := runtime.NewScheme() + kscheme.AddToScheme(scheme) + + gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind) + out, err := scheme.New(gvk) + if err != nil { + return nil, err + } + out.GetObjectKind().SetGroupVersionKind(gvk) + return out, nil +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/deprecations_test.go b/pkg/helm/pkg/chart/v2/lint/rules/deprecations_test.go new file mode 100644 index 00000000..dbba1bd1 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/deprecations_test.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules // import "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/rules" + +import "testing" + +func TestValidateNoDeprecations(t *testing.T) { + deprecated := &k8sYamlStruct{ + APIVersion: "extensions/v1beta1", + Kind: "Deployment", + } + err := validateNoDeprecations(deprecated, nil) + if err == nil { + t.Fatal("Expected deprecated extension to be flagged") + } + depErr := err.(deprecatedAPIError) + if depErr.Message == "" { + t.Fatalf("Expected error message to be non-blank: %v", err) + } + + if err := validateNoDeprecations(&k8sYamlStruct{ + APIVersion: "v1", + Kind: "Pod", + }, nil); err != nil { + t.Errorf("Expected a v1 Pod to not be deprecated") + } +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/template.go b/pkg/helm/pkg/chart/v2/lint/rules/template.go new file mode 100644 index 00000000..bfeeef50 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/template.go @@ -0,0 +1,385 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "slices" + "strings" + + "k8s.io/apimachinery/pkg/api/validation" + apipath "k8s.io/apimachinery/pkg/api/validation/path" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/apimachinery/pkg/util/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" + "github.com/werf/nelm/pkg/helm/pkg/engine" +) + +// Templates lints the templates in the Linter. +func Templates(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) { + templateLinter := newTemplateLinter(linter, namespace, values, options...) + templateLinter.Lint() +} + +type TemplateLinterOption func(*templateLinter) + +func TemplateLinterKubeVersion(kubeVersion *common.KubeVersion) TemplateLinterOption { + return func(tl *templateLinter) { + tl.kubeVersion = kubeVersion + } +} + +func TemplateLinterSkipSchemaValidation(skipSchemaValidation bool) TemplateLinterOption { + return func(tl *templateLinter) { + tl.skipSchemaValidation = skipSchemaValidation + } +} + +func newTemplateLinter(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) templateLinter { + + result := templateLinter{ + linter: linter, + values: values, + namespace: namespace, + } + + for _, o := range options { + o(&result) + } + + return result +} + +type templateLinter struct { + linter *support.Linter + values map[string]any + namespace string + kubeVersion *common.KubeVersion + skipSchemaValidation bool +} + +func (t *templateLinter) Lint() { + templatesDir := "templates/" + templatesPath := filepath.Join(t.linter.ChartDir, templatesDir) + + templatesDirExists := t.linter.RunLinterRule(support.WarningSev, templatesDir, templatesDirExists(templatesPath)) + if !templatesDirExists { + return + } + + validTemplatesDir := t.linter.RunLinterRule(support.ErrorSev, templatesDir, validateTemplatesDir(templatesPath)) + if !validTemplatesDir { + return + } + + // Load chart and parse templates + chart, err := loader.Load(context.Background(), t.linter.ChartDir) + + chartLoaded := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) + + if !chartLoaded { + return + } + + options := common.ReleaseOptions{ + Name: "test-release", + Namespace: t.namespace, + } + + caps := common.DefaultCapabilities.Copy() + if t.kubeVersion != nil { + caps.KubeVersion = *t.kubeVersion + } + + // lint ignores import-values + // See https://github.com/helm/helm/issues/9658 + if err := chartutil.ProcessDependencies(chart, &t.values); err != nil { + return + } + + cvals, err := util.CoalesceValues(chart, t.values) + if err != nil { + return + } + + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, t.skipSchemaValidation) + if err != nil { + t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) + return + } + var e engine.Engine + e.LintMode = true + renderedContentMap, err := e.Render(context.Background(), chart, valuesToRender) + + renderOk := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) + + if !renderOk { + return + } + + /* Iterate over all the templates to check: + - It is a .yaml file + - All the values in the template file is defined + - {{}} include | quote + - Generated content is a valid Yaml file + - Metadata.Namespace is not set + */ + for _, template := range chart.Templates { + fileName := template.Name + + t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName)) + + // We only apply the following lint rules to yaml files + if !isYamlFileExtension(fileName) { + continue + } + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 + // Check that all the templates have a matching value + // linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) + + // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 + // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) + + renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] + if strings.TrimSpace(renderedContent) != "" { + t.linter.RunLinterRule(support.WarningSev, fileName, validateTopIndentLevel(renderedContent)) + + decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) + + // Lint all resources if the file contains multiple documents separated by --- + for { + // Even though k8sYamlStruct only defines a few fields, an error in any other + // key will be raised as well + var yamlStruct *k8sYamlStruct + + err := decoder.Decode(&yamlStruct) + if errors.Is(err, io.EOF) { + break + } + + // If YAML linting fails here, it will always fail in the next block as well, so we should return here. + // fix https://github.com/helm/helm/issues/11391 + if !t.linter.RunLinterRule(support.ErrorSev, fileName, validateYamlContent(err)) { + return + } + if yamlStruct != nil { + // NOTE: set to warnings to allow users to support out-of-date kubernetes + // Refs https://github.com/helm/helm/issues/8596 + t.linter.RunLinterRule(support.WarningSev, fileName, validateMetadataName(yamlStruct)) + t.linter.RunLinterRule(support.WarningSev, fileName, validateNoDeprecations(yamlStruct, t.kubeVersion)) + + t.linter.RunLinterRule(support.ErrorSev, fileName, validateMatchSelector(yamlStruct, renderedContent)) + t.linter.RunLinterRule(support.ErrorSev, fileName, validateListAnnotations(yamlStruct, renderedContent)) + } + } + } + } +} + +// validateTopIndentLevel checks that the content does not start with an indent level > 0. +// +// This error can occur when a template accidentally inserts space. It can cause +// unpredictable errors depending on whether the text is normalized before being passed +// into the YAML parser. So we trap it here. +// +// See https://github.com/helm/helm/issues/8467 +func validateTopIndentLevel(content string) error { + // Read lines until we get to a non-empty one + scanner := bufio.NewScanner(bytes.NewBufferString(content)) + for scanner.Scan() { + line := scanner.Text() + // If line is empty, skip + if strings.TrimSpace(line) == "" { + continue + } + // If it starts with one or more spaces, this is an error + if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { + return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line) + } + // Any other condition passes. + return nil + } + return scanner.Err() +} + +// Validation functions +func templatesDirExists(templatesPath string) error { + _, err := os.Stat(templatesPath) + if errors.Is(err, os.ErrNotExist) { + return errors.New("directory does not exist") + } + return nil +} + +func validateTemplatesDir(templatesPath string) error { + fi, err := os.Stat(templatesPath) + if err != nil { + return err + } + if !fi.IsDir() { + return errors.New("not a directory") + } + return nil +} + +func validateAllowedExtension(fileName string) error { + ext := filepath.Ext(fileName) + validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"} + + if slices.Contains(validExtensions, ext) { + return nil + } + + return fmt.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) +} + +func validateYamlContent(err error) error { + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + + return nil +} + +// validateMetadataName uses the correct validation function for the object +// Kind, or if not set, defaults to the standard definition of a subdomain in +// DNS (RFC 1123), used by most resources. +func validateMetadataName(obj *k8sYamlStruct) error { + fn := validateMetadataNameFunc(obj) + allErrs := field.ErrorList{} + for _, msg := range fn(obj.Metadata.Name, false) { + allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg)) + } + if len(allErrs) > 0 { + return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q: %w", obj.Metadata.Name, allErrs.ToAggregate()) + } + return nil +} + +// validateMetadataNameFunc will return a name validation function for the +// object kind, if defined below. +// +// Rules should match those set in the various api validations: +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274 +// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39 +// ... +// +// Implementing here to avoid importing k/k. +// +// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object +// kinds that don't have special requirements, so is the most likely to work if +// new kinds are added. +func validateMetadataNameFunc(obj *k8sYamlStruct) validation.ValidateNameFunc { + switch strings.ToLower(obj.Kind) { + case "pod", "node", "secret", "endpoints", "resourcequota", // core + "controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps + "autoscaler", // autoscaler + "cronjob", "job", // batch + "lease", // coordination + "endpointslice", // discovery + "networkpolicy", "ingress", // networking + "podsecuritypolicy", // policy + "priorityclass", // scheduling + "podpreset", // settings + "storageclass", "volumeattachment", "csinode": // storage + return validation.NameIsDNSSubdomain + case "service": + return validation.NameIsDNS1035Label + case "namespace": + return validation.ValidateNamespaceName + case "serviceaccount": + return validation.ValidateServiceAccountName + case "certificatesigningrequest": + // No validation. + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140 + return func(_ string, _ bool) []string { return nil } + case "role", "clusterrole", "rolebinding", "clusterrolebinding": + // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 + return func(name string, _ bool) []string { + return apipath.IsValidPathSegmentName(name) + } + default: + return validation.NameIsDNSSubdomain + } +} + +// validateMatchSelector ensures that template specs have a selector declared. +// See https://github.com/helm/helm/issues/1990 +func validateMatchSelector(yamlStruct *k8sYamlStruct, manifest string) error { + switch yamlStruct.Kind { + case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet": + // verify that matchLabels or matchExpressions is present + if !strings.Contains(manifest, "matchLabels") && !strings.Contains(manifest, "matchExpressions") { + return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name) + } + } + return nil +} + +func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error { + if yamlStruct.Kind == "List" { + m := struct { + Items []struct { + Metadata struct { + Annotations map[string]string + } + } + }{} + + if err := yaml.Unmarshal([]byte(manifest), &m); err != nil { + return validateYamlContent(err) + } + + for _, i := range m.Items { + if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok { + return errors.New("annotation 'helm.sh/resource-policy' within List objects are ignored") + } + } + } + return nil +} + +func isYamlFileExtension(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + return ext == ".yaml" || ext == ".yml" +} + +// k8sYamlStruct stubs a Kubernetes YAML file. +type k8sYamlStruct struct { + APIVersion string `json:"apiVersion"` + Kind string + Metadata k8sYamlMetadata +} + +type k8sYamlMetadata struct { + Namespace string + Name string +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/template_test.go b/pkg/helm/pkg/chart/v2/lint/rules/template_test.go new file mode 100644 index 00000000..f757d6ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/template_test.go @@ -0,0 +1,490 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" +) + +const templateTestBasedir = "./testdata/albatross" + +func TestValidateAllowedExtension(t *testing.T) { + var failTest = []string{"/foo", "/test.toml"} + for _, test := range failTest { + err := validateAllowedExtension(test) + if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") { + t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test) + } + } + var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"} + for _, test := range successTest { + err := validateAllowedExtension(test) + if err != nil { + t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error()) + } + } +} + +var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} + +const namespace = "testNamespace" + +func TestTemplateParsing(t *testing.T) { + linter := support.Linter{ChartDir: templateTestBasedir} + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected one error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") { + t.Errorf("Unexpected error: %s", res[0]) + } +} + +var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml") +var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored") + +// Test a template with all the existing features: +// namespaces, partial templates +func TestTemplateIntegrationHappyPath(t *testing.T) { + // Rename file so it gets ignored by the linter + os.Rename(wrongTemplatePath, ignoredTemplatePath) + defer os.Rename(ignoredTemplatePath, wrongTemplatePath) + + linter := support.Linter{ChartDir: templateTestBasedir} + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) + res := linter.Messages + + if len(res) != 0 { + t.Fatalf("Expected no error, got %d, %v", len(res), res) + } +} + +func TestMultiTemplateFail(t *testing.T) { + linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) + res := linter.Messages + + if len(res) != 1 { + t.Fatalf("Expected 1 error, got %d, %v", len(res), res) + } + + if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") { + t.Errorf("Unexpected error: %s", res[0].Err) + } +} + +func TestValidateMetadataName(t *testing.T) { + tests := []struct { + obj *k8sYamlStruct + wantErr bool + }{ + // Most kinds use IsDNS1123Subdomain. + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true}, + + // Service uses IsDNS1035Label. + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true}, + {&k8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + + // Namespace uses IsDNS1123Label. + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, + {&k8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false}, + + // CertificateSigningRequest has no validation. + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false}, + + // RBAC uses path validation. + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, + {&k8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, + {&k8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + {&k8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, + + // Unknown Kind + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, + {&k8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + + // No kind + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false}, + {&k8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, + } + for _, tt := range tests { + t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) { + if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr { + t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDeprecatedAPIFails(t *testing.T) { + modTime := time.Now() + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "failapi", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/baddeployment.yaml", + ModTime: modTime, + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + }, + { + Name: "templates/goodsecret.yaml", + ModTime: modTime, + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) + if l := len(linter.Messages); l != 1 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 1 lint error, got %d", l) + } + + err := linter.Messages[0].Err.(deprecatedAPIError) + if err.Deprecated != "apps/v1beta1 Deployment" { + t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) + } +} + +const manifest = `apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + myval1: {{default "val" .Values.mymap.key1 }} + myval2: {{default "val" .Values.mymap.key2 }} +` + +// TestStrictTemplateParsingMapError is a regression test. +// +// The template engine should not produce an error when a map in values.yaml does +// not contain all possible keys. +// +// See https://github.com/helm/helm/issues/7483 +func TestStrictTemplateParsingMapError(t *testing.T) { + + ch := chart.Chart{ + Metadata: &chart.Metadata{ + Name: "regression7483", + APIVersion: "v2", + Version: "0.1.0", + }, + Values: map[string]interface{}{ + "mymap": map[string]string{ + "key1": "val1", + }, + }, + Templates: []*common.File{ + { + Name: "templates/configmap.yaml", + ModTime: time.Now(), + Data: []byte(manifest), + }, + }, + } + dir := t.TempDir() + if err := chartutil.SaveDir(&ch, dir); err != nil { + t.Fatal(err) + } + linter := &support.Linter{ + ChartDir: filepath.Join(dir, ch.Metadata.Name), + } + Templates( + linter, + namespace, + ch.Values, + TemplateLinterSkipSchemaValidation(false)) + if len(linter.Messages) != 0 { + t.Errorf("expected zero messages, got %d", len(linter.Messages)) + for i, msg := range linter.Messages { + t.Logf("Message %d: %q", i, msg) + } + } +} + +func TestValidateMatchSelector(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "apps/v1", + Kind: "Deployment", + Metadata: k8sYamlMetadata{ + Name: "mydeployment", + }, + } + manifest := ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchExpressions: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err != nil { + t.Error(err) + } + manifest = ` + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ` + if err := validateMatchSelector(md, manifest); err == nil { + t.Error("expected Deployment with no selector to fail") + } +} + +func TestValidateTopIndentLevel(t *testing.T) { + for doc, shouldFail := range map[string]bool{ + // Should not fail + "\n\n\n\t\n \t\n": false, + "apiVersion:foo\n bar:baz": false, + "\n\n\napiVersion:foo\n\n\n": false, + // Should fail + " apiVersion:foo": true, + "\n\n apiVersion:foo\n\n": true, + } { + if err := validateTopIndentLevel(doc); (err == nil) == shouldFail { + t.Errorf("Expected %t for %q", shouldFail, doc) + } + } + +} + +// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments +// See https://github.com/helm/helm/issues/8621 +func TestEmptyWithCommentsManifests(t *testing.T) { + mychart := chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "emptymanifests", + Version: "0.1.0", + Icon: "satisfy-the-linting-gods.gif", + }, + Templates: []*common.File{ + { + Name: "templates/empty-with-comments.yaml", + ModTime: time.Now(), + Data: []byte("#@formatter:off\n"), + }, + }, + } + tmpdir := t.TempDir() + + if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { + t.Fatal(err) + } + + linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) + if l := len(linter.Messages); l > 0 { + for i, msg := range linter.Messages { + t.Logf("Message %d: %s", i, msg) + } + t.Fatalf("Expected 0 lint errors, got %d", l) + } +} +func TestValidateListAnnotations(t *testing.T) { + md := &k8sYamlStruct{ + APIVersion: "v1", + Kind: "List", + Metadata: k8sYamlMetadata{ + Name: "list", + }, + } + manifest := ` +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: ConfigMap + metadata: + annotations: + helm.sh/resource-policy: keep +` + + if err := validateListAnnotations(md, manifest); err == nil { + t.Fatal("expected list with nested keep annotations to fail") + } + + manifest = ` +apiVersion: v1 +kind: List +metadata: + annotations: + helm.sh/resource-policy: keep +items: + - apiVersion: v1 + kind: ConfigMap +` + + if err := validateListAnnotations(md, manifest); err != nil { + t.Fatalf("List objects keep annotations should pass. got: %s", err) + } +} + +func TestIsYamlFileExtension(t *testing.T) { + tests := []struct { + filename string + expected bool + }{ + {"test.yaml", true}, + {"test.yml", true}, + {"test.txt", false}, + {"test", false}, + } + + for _, test := range tests { + result := isYamlFileExtension(test.filename) + if result != test.expected { + t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected) + } + } + +} diff --git a/pkg/helm/pkg/lint/rules/testdata/albatross/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/albatross/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl new file mode 100644 index 00000000..24f76db7 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/_helpers.tpl @@ -0,0 +1,16 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{define "name"}}{{default "nginx" .Values.nameOverride | trunc 63 | trimSuffix "-" }}{{end}} + +{{/* +Create a default fully qualified app name. + +We truncate at 63 chars because some Kubernetes name fields are limited to this +(by the DNS naming spec). +*/}} +{{define "fullname"}} +{{- $name := default "nginx" .Values.nameOverride -}} +{{printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{end}} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml new file mode 100644 index 00000000..a11e0e90 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/fail.yaml @@ -0,0 +1 @@ +{{ deliberateSyntaxError }} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml new file mode 100644 index 00000000..16bb27d5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/templates/svc.yaml @@ -0,0 +1,19 @@ +# This is a service gateway to the replica set created by the deployment. +# Take a look at the deployment.yaml for general notes about this chart. +apiVersion: v1 +kind: Service +metadata: + name: "{{ .Values.name }}" + labels: + app.kubernetes.io/managed-by: {{ .Release.Service | quote }} + app.kubernetes.io/instance: {{ .Release.Name | quote }} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" + kubeVersion: {{ .Capabilities.KubeVersion.Major }} +spec: + ports: + - port: {{default 80 .Values.httpPort | quote}} + targetPort: 80 + protocol: TCP + name: http + selector: + app.kubernetes.io/name: {{template "fullname" .}} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml new file mode 100644 index 00000000..74cc6a0d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/albatross/values.yaml @@ -0,0 +1 @@ +name: "mariner" diff --git a/pkg/helm/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/anotherbadchartfile/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/anotherbadchartfile/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/anotherbadchartfile/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml new file mode 100644 index 00000000..3564ede3 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartfile/Chart.yaml @@ -0,0 +1,11 @@ +description: A Helm chart for Kubernetes +version: 0.0.0.0 +home: "" +type: application +dependencies: +- name: mariadb + version: 5.x.x + repository: https://charts.helm.sh/stable/ + condition: mariadb.enabled + tags: + - database diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml new file mode 100644 index 00000000..9f367033 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartfile/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. diff --git a/pkg/helm/pkg/lint/rules/testdata/badchartname/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartname/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badchartname/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartname/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml new file mode 100644 index 00000000..9f367033 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badchartname/values.yaml @@ -0,0 +1 @@ +# Default values for badchartfile. diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml new file mode 100644 index 00000000..08c4b61a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: badcrdfile +type: application +icon: http://riverrun.io diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml new file mode 100644 index 00000000..46891605 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-apiversion.yaml @@ -0,0 +1,2 @@ +apiVersion: bad.k8s.io/v1beta1 +kind: CustomResourceDefinition diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml new file mode 100644 index 00000000..523b97f8 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/crds/bad-crd.yaml @@ -0,0 +1,2 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: NotACustomResourceDefinition diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/repository/test-name-charts.txt b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/repository/test-name-charts.txt rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/templates/.gitkeep diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml new file mode 100644 index 00000000..2fffc771 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badcrdfile/values.yaml @@ -0,0 +1 @@ +# Default values for badcrdfile. diff --git a/pkg/helm/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/badvaluesfile/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml new file mode 100644 index 00000000..6c2ceb8d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/templates/badvaluesfile.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{.name | default "foo" | title}} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml new file mode 100644 index 00000000..b5a10271 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/badvaluesfile/values.yaml @@ -0,0 +1,2 @@ +# Invalid value for badvaluesfile for testing lint fails with invalid yaml format +name= "value" diff --git a/pkg/helm/pkg/lint/rules/testdata/goodone/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/goodone/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml new file mode 100644 index 00000000..1d7350f1 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml new file mode 100644 index 00000000..cd46f62c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/templates/goodone.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.name | default "foo" | lower }} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml new file mode 100644 index 00000000..92c3d9bb --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/goodone/values.yaml @@ -0,0 +1 @@ +name: "goodone-here" diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml new file mode 100644 index 00000000..0fd58d1d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidchartfile/Chart.yaml @@ -0,0 +1,6 @@ +name: some-chart +apiVersion: v2 +apiVersion: v1 +description: A Helm chart for Kubernetes +version: 1.3.0 +icon: http://example.com diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/values.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidchartfile/values.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml new file mode 100644 index 00000000..18e30f70 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +description: A Helm chart for Kubernetes +version: 0.1.0 +name: invalidcrdsdir +type: application +icon: http://riverrun.io diff --git a/pkg/helm/cmd/helm/testdata/output/lint-quiet-with-warning.txt b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/lint-quiet-with-warning.txt rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/crds diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml new file mode 100644 index 00000000..6b1611a6 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/invalidcrdsdir/values.yaml @@ -0,0 +1 @@ +# Default values for invalidcrdsdir. diff --git a/pkg/helm/pkg/lint/rules/testdata/malformed-template/.helmignore b/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/malformed-template/.helmignore rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/.helmignore diff --git a/pkg/helm/pkg/lint/rules/testdata/malformed-template/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/malformed-template/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml new file mode 100644 index 00000000..213198fd --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/templates/bad.yaml @@ -0,0 +1 @@ +{ {- $relname := .Release.Name -}} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml new file mode 100644 index 00000000..1cc3182e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/malformed-template/values.yaml @@ -0,0 +1,82 @@ +# Default values for test. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/helm/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/multi-template-fail/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/multi-template-fail/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/multi-template-fail/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml new file mode 100644 index 00000000..835be07b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/multi-template-fail/templates/multi-fail.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: game-config +data: + game.properties: cheat +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: -this:name-is-not_valid$ +data: + game.properties: empty diff --git a/pkg/helm/pkg/lint/rules/testdata/v3-fail/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/v3-fail/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl new file mode 100644 index 00000000..0b89e723 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "v3-fail.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "v3-fail.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "v3-fail.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "v3-fail.labels" -}} +helm.sh/chart: {{ include "v3-fail.chart" . }} +{{ include "v3-fail.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Selector labels +*/}} +{{- define "v3-fail.selectorLabels" -}} +app.kubernetes.io/name: {{ include "v3-fail.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "v3-fail.serviceAccountName" -}} +{{- if .Values.serviceAccount.create -}} + {{ default (include "v3-fail.fullname" .) .Values.serviceAccount.name }} +{{- else -}} + {{ default "default" .Values.serviceAccount.name }} +{{- end -}} +{{- end -}} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml new file mode 100644 index 00000000..6d651ab8 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/deployment.yaml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "v3-fail.fullname" . }} + labels: + nope: {{ .Release.Time }} + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "v3-fail.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "v3-fail.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "v3-fail.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml new file mode 100644 index 00000000..4790650d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/ingress.yaml @@ -0,0 +1,62 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "v3-fail.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "v3-fail.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + "helm.sh/hook": crd-install + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml new file mode 100644 index 00000000..79a0f40b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "v3-fail.fullname" . }} + annotations: + helm.sh/hook: crd-install + labels: + {{- include "v3-fail.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "v3-fail.selectorLabels" . | nindent 4 }} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml new file mode 100644 index 00000000..01d99b4e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/v3-fail/values.yaml @@ -0,0 +1,66 @@ +# Default values for v3-fail. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/helm/pkg/lint/rules/testdata/withsubchart/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/withsubchart/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/Chart.yaml diff --git a/pkg/helm/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml new file mode 100644 index 00000000..6cb6cc2a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/templates/subchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml new file mode 100644 index 00000000..422a359d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/charts/subchart/values.yaml @@ -0,0 +1,2 @@ +subchart: + name: subchart \ No newline at end of file diff --git a/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml new file mode 100644 index 00000000..6cb6cc2a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/templates/mainchart.yaml @@ -0,0 +1,2 @@ +metadata: + name: {{ .Values.subchart.name | lower }} diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-deprecated-api/values.yaml b/pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-deprecated-api/values.yaml rename to pkg/helm/pkg/chart/v2/lint/rules/testdata/withsubchart/values.yaml diff --git a/pkg/helm/pkg/chart/v2/lint/rules/values.go b/pkg/helm/pkg/chart/v2/lint/rules/values.go new file mode 100644 index 00000000..320ee6c2 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/values.go @@ -0,0 +1,84 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" +) + +// ValuesWithOverrides tests the values.yaml file. +// +// If a schema is present in the chart, values are tested against that. Otherwise, +// they are only tested for well-formedness. +// +// If additional values are supplied, they are coalesced into the values in values.yaml. +func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) { + file := "values.yaml" + vf := filepath.Join(linter.ChartDir, file) + fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) + + if !fileExists { + return + } + + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation)) +} + +func validateValuesFileExistence(valuesPath string) error { + _, err := os.Stat(valuesPath) + if err != nil { + return fmt.Errorf("file does not exist") + } + return nil +} + +func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error { + values, err := common.ReadValuesFile(valuesPath) + if err != nil { + return fmt.Errorf("unable to parse YAML: %w", err) + } + + // Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top + // level values against the top-level expectations. Subchart values are not linted. + // We could change that. For now, though, we retain that strategy, and thus can + // coalesce tables (like reuse-values does) instead of doing the full chart + // CoalesceValues + coalescedValues := util.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) + coalescedValues = util.CoalesceTables(coalescedValues, values) + + ext := filepath.Ext(valuesPath) + schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" + schema, err := os.ReadFile(schemaPath) + if len(schema) == 0 { + return nil + } + if err != nil { + return err + } + + if !skipSchemaValidation { + return util.ValidateAgainstSingleSchema(coalescedValues, schema) + } + + return nil +} diff --git a/pkg/helm/pkg/chart/v2/lint/rules/values_test.go b/pkg/helm/pkg/chart/v2/lint/rules/values_test.go new file mode 100644 index 00000000..a2a5345d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/rules/values_test.go @@ -0,0 +1,183 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/intern/test/ensure" +) + +var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") + +const testSchema = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "helm values test schema", + "type": "object", + "additionalProperties": false, + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "description": "Your username", + "type": "string" + }, + "password": { + "description": "Your password", + "type": "string" + } + } +} +` + +func TestValidateValuesYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) + defer os.Remove(nonExistingValuesFilePath) + + err := validateValuesFileExistence(nonExistingValuesFilePath) + if err == nil { + t.Errorf("validateValuesFileExistence to return a linter error, got no error") + } +} + +func TestValidateValuesFileWellFormed(t *testing.T) { + badYaml := ` + not:well[]{}formed + ` + tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil { + t.Fatal("expected values file to fail parsing") + } +} + +func TestValidateValuesFileSchema(t *testing.T) { + yaml := "username: admin\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFileSchemaFailure(t *testing.T) { + // 1234 is an int, not a string. This should fail. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}, false) + if err == nil { + t.Fatal("expected values file to fail parsing") + } + + assert.Contains(t, err.Error(), "- at '/username': got number, want string") +} + +func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) { + // 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}, true) + if err != nil { + t.Fatal("expected values file to pass parsing because of skipSchemaValidation") + } +} + +func TestValidateValuesFileSchemaOverrides(t *testing.T) { + yaml := "username: admin" + overrides := map[string]interface{}{ + "password": "swordfish", + } + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + if err := validateValuesFile(valfile, overrides, false); err != nil { + t.Fatalf("Failed validation with %s", err) + } +} + +func TestValidateValuesFile(t *testing.T) { + tests := []struct { + name string + yaml string + overrides map[string]interface{} + errorMessage string + }{ + { + name: "value added", + yaml: "username: admin", + overrides: map[string]interface{}{"password": "swordfish"}, + }, + { + name: "value not overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser"}, + errorMessage: "- at '/password': got null, want string", + }, + { + name: "value overridden", + yaml: "username: admin\npassword:", + overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpdir := ensure.TempFile(t, "values.yaml", []byte(tt.yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, tt.overrides, false) + + switch { + case err != nil && tt.errorMessage == "": + t.Errorf("Failed validation with %s", err) + case err == nil && tt.errorMessage != "": + t.Error("expected values file to fail parsing") + case err != nil && tt.errorMessage != "": + assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error") + } + }) + } +} + +func createTestingSchema(t *testing.T, dir string) string { + t.Helper() + schemafile := filepath.Join(dir, "values.schema.json") + if err := os.WriteFile(schemafile, []byte(testSchema), 0700); err != nil { + t.Fatalf("Failed to write schema to tmpdir: %s", err) + } + return schemafile +} diff --git a/pkg/helm/pkg/chart/v2/lint/support/doc.go b/pkg/helm/pkg/chart/v2/lint/support/doc.go new file mode 100644 index 00000000..8c8dced7 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/support/doc.go @@ -0,0 +1,23 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package support contains tools for linting charts. + +Linting is the process of testing charts for errors or warnings regarding +formatting, compilation, or standards compliance. +*/ +package support // import "github.com/werf/nelm/pkg/helm/pkg/chart/v2/lint/support" diff --git a/pkg/helm/pkg/chart/v2/lint/support/message.go b/pkg/helm/pkg/chart/v2/lint/support/message.go new file mode 100644 index 00000000..5efbc7a6 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/support/message.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import "fmt" + +// Severity indicates the severity of a Message. +const ( + // UnknownSev indicates that the severity of the error is unknown, and should not stop processing. + UnknownSev = iota + // InfoSev indicates information, for example missing values.yaml file + InfoSev + // WarningSev indicates that something does not meet code standards, but will likely function. + WarningSev + // ErrorSev indicates that something will not likely function. + ErrorSev +) + +// sev matches the *Sev states. +var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} + +// Linter encapsulates a linting run of a particular chart. +type Linter struct { + Messages []Message + // The highest severity of all the failing lint rules + HighestSeverity int + ChartDir string +} + +// Message describes an error encountered while linting. +type Message struct { + // Severity is one of the *Sev constants + Severity int + Path string + Err error +} + +func (m Message) Error() string { + return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) +} + +// NewMessage creates a new Message struct +func NewMessage(severity int, path string, err error) Message { + return Message{Severity: severity, Path: path, Err: err} +} + +// RunLinterRule returns true if the validation passed +func (l *Linter) RunLinterRule(severity int, path string, err error) bool { + // severity is out of bound + if severity < 0 || severity >= len(sev) { + return false + } + + if err != nil { + l.Messages = append(l.Messages, NewMessage(severity, path, err)) + + if severity > l.HighestSeverity { + l.HighestSeverity = severity + } + } + return err == nil +} diff --git a/pkg/helm/pkg/chart/v2/lint/support/message_test.go b/pkg/helm/pkg/chart/v2/lint/support/message_test.go new file mode 100644 index 00000000..ce5b5e42 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/lint/support/message_test.go @@ -0,0 +1,79 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package support + +import ( + "errors" + "testing" +) + +var errLint = errors.New("lint failed") + +func TestRunLinterRule(t *testing.T) { + var tests = []struct { + Severity int + LintError error + ExpectedMessages int + ExpectedReturn bool + ExpectedHighestSeverity int + }{ + {InfoSev, errLint, 1, false, InfoSev}, + {WarningSev, errLint, 2, false, WarningSev}, + {ErrorSev, errLint, 3, false, ErrorSev}, + // No error so it returns true + {ErrorSev, nil, 3, true, ErrorSev}, + // Retains highest severity + {InfoSev, errLint, 4, false, ErrorSev}, + // Invalid severity values + {4, errLint, 4, false, ErrorSev}, + {22, errLint, 4, false, ErrorSev}, + {-1, errLint, 4, false, ErrorSev}, + } + + linter := Linter{} + for _, test := range tests { + isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError) + if len(linter.Messages) != test.ExpectedMessages { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) + } + + if linter.HighestSeverity != test.ExpectedHighestSeverity { + t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) + } + + if isValid != test.ExpectedReturn { + t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) + } + } +} + +func TestMessage(t *testing.T) { + m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")} + if m.Error() != "[ERROR] Chart.yaml: Foo" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{WarningSev, "templates/", errors.New("Bar")} + if m.Error() != "[WARNING] templates/: Bar" { + t.Errorf("Unexpected output: %s", m.Error()) + } + + m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")} + if m.Error() != "[INFO] templates/rc.yaml: FooBar" { + t.Errorf("Unexpected output: %s", m.Error()) + } +} diff --git a/pkg/helm/pkg/chart/v2/loader/archive.go b/pkg/helm/pkg/chart/v2/loader/archive.go new file mode 100644 index 00000000..04af3711 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/archive.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "os" + + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +// FileLoader loads a chart from a file +type FileLoader string + +// Load loads a chart +func (l FileLoader) Load(ctx context.Context) (*chart.Chart, error) { + return LoadFile(ctx, string(l)) +} + +// LoadFile loads from an archive file. +func LoadFile(ctx context.Context, name string) (*chart.Chart, error) { + + if fi, err := os.Stat(name); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory") + } + + raw, err := os.Open(name) + if err != nil { + return nil, err + } + defer raw.Close() + + err = archive.EnsureArchive(name, raw) + if err != nil { + return nil, err + } + + c, err := LoadArchive(ctx, raw) + if err != nil { + if errors.Is(err, gzip.ErrHeader) { + return nil, fmt.Errorf("file '%s' does not appear to be a valid chart file (details: %w)", name, err) + } + } + return c, err +} + +// LoadArchive loads from a reader containing a compressed tar archive. +func LoadArchive(ctx context.Context, in io.Reader) (*chart.Chart, error) { + files, err := archive.LoadArchiveFiles(in) + if err != nil { + return nil, err + } + + return LoadFiles(ctx, files) +} diff --git a/pkg/helm/pkg/chart/v2/loader/chart_metadata.go b/pkg/helm/pkg/chart/v2/loader/chart_metadata.go new file mode 100644 index 00000000..48776d19 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/chart_metadata.go @@ -0,0 +1,37 @@ +package loader + +import chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + +type autosetChartMetadataOptions struct { + OverrideAppVersion string + DefaultAPIVersion string + DefaultName string + DefaultVersion string +} + +func autosetChartMetadata(metadataIn *chart.Metadata, opts autosetChartMetadataOptions) *chart.Metadata { + var metadata *chart.Metadata + if metadataIn == nil { + metadata = &chart.Metadata{} + } else { + metadata = metadataIn + } + + if metadata.APIVersion == "" { + metadata.APIVersion = opts.DefaultAPIVersion + } + + if metadata.Name == "" { + metadata.Name = opts.DefaultName + } + + if opts.OverrideAppVersion != "" { + metadata.AppVersion = opts.OverrideAppVersion + } + + if metadata.Version == "" { + metadata.Version = opts.DefaultVersion + } + + return metadata +} diff --git a/pkg/helm/pkg/chart/v2/loader/directory.go b/pkg/helm/pkg/chart/v2/loader/directory.go new file mode 100644 index 00000000..9cf58e3a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/directory.go @@ -0,0 +1,159 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "strings" + + nelmcommon "github.com/werf/nelm/pkg/common" + "github.com/werf/nelm/pkg/helm/intern/sympath" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/ignore" +) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// DirLoader loads a chart from a directory +type DirLoader string + +// Load loads the chart +func (l DirLoader) Load(ctx context.Context) (*chart.Chart, error) { + return LoadDir(ctx, string(l)) +} + +// LoadDir loads from a directory. +// +// This loads charts only from directories. +func LoadDir(ctx context.Context, dir string) (*chart.Chart, error) { + helmOpts := nelmcommon.HelmOptionsFromContext(ctx) + + var files []*archive.BufferedFile + switch helmOpts.ChartLoadOpts.ChartType { + case nelmcommon.LegacyChartTypeChart: + if nelmcommon.ChartFileReader != nil { + chartFiles, err := nelmcommon.ChartFileReader.LoadChartDir(ctx, dir) + if err != nil { + return nil, fmt.Errorf("load chart dir: %w", err) + } + + files = make([]*archive.BufferedFile, 0, len(chartFiles)) + for _, f := range chartFiles { + files = append(files, &archive.BufferedFile{Name: f.Name, Data: f.Data}) + } + } else { + localFiles, err := getFilesFromLocalFilesystem(dir) + if err != nil { + return nil, err + } + + files = localFiles + } + case nelmcommon.LegacyChartTypeBundle, nelmcommon.LegacyChartTypeSubchart, nelmcommon.LegacyChartTypeChartStub: + localFiles, err := getFilesFromLocalFilesystem(dir) + if err != nil { + return nil, err + } + + files = localFiles + default: + panic("unexpected type") + } + + return LoadFiles(ctx, files) +} + +func getFilesFromLocalFilesystem(dir string) ([]*archive.BufferedFile, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + rules := ignore.Empty() + ifile := filepath.Join(topdir, ignore.HelmIgnore) + if _, err := os.Stat(ifile); err == nil { + r, err := ignore.ParseFile(ifile) + if err != nil { + return nil, err + } + rules = r + } + rules.AddDefaults() + + files := []*archive.BufferedFile{} + topdir += string(filepath.Separator) + + walk := func(name string, fi os.FileInfo, err error) error { + n := strings.TrimPrefix(name, topdir) + if n == "" { + // No need to process top level. Avoid bug with helmignore .* matching + // empty names. See issue 1779. + return nil + } + + // Normalize to / since it will also work on Windows + n = filepath.ToSlash(n) + + if err != nil { + return err + } + if fi.IsDir() { + // Directory-based ignore rules should involve skipping the entire + // contents of that directory. + if rules.Ignore(n, fi) { + return filepath.SkipDir + } + return nil + } + + // If a .helmignore file matches, skip this file. + if rules.Ignore(n, fi) { + return nil + } + + // Irregular files include devices, sockets, and other uses of files that + // are not regular files. In Go they have a file mode type bit set. + // See https://golang.org/pkg/os/#FileMode for examples. + if !fi.Mode().IsRegular() { + return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) + } + + if fi.Size() > archive.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) + } + + data, err := os.ReadFile(name) + if err != nil { + return fmt.Errorf("error reading %s: %w", n, err) + } + + data = bytes.TrimPrefix(data, utf8bom) + + files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data}) + return nil + } + if err = sympath.Walk(topdir, walk); err != nil { + return nil, err + } + + return files, nil +} diff --git a/pkg/helm/pkg/chart/v2/loader/load.go b/pkg/helm/pkg/chart/v2/loader/load.go new file mode 100644 index 00000000..e751510e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/load.go @@ -0,0 +1,380 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "bufio" + "bytes" + "context" + "errors" + "fmt" + "io" + "log" + "maps" + "os" + "path/filepath" + "strings" + + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" + + "github.com/werf/common-go/pkg/secrets_manager" + nelmcommon "github.com/werf/nelm/pkg/common" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + legacysecret "github.com/werf/nelm/pkg/legacy/secret" +) + +// ChartLoader loads a chart. +type ChartLoader interface { + Load(ctx context.Context) (*chart.Chart, error) +} + +// Loader returns a new ChartLoader appropriate for the given chart name +func Loader(name string) (ChartLoader, error) { + fi, err := os.Stat(name) + if err != nil { + return nil, err + } + if fi.IsDir() { + return DirLoader(name), nil + } + return FileLoader(name), nil +} + +// Load takes a string name, tries to resolve it to a file or directory, and then loads it. +// +// This is the preferred way to load a chart. It will discover the chart encoding +// and hand off to the appropriate chart reader. +// +// If a .helmignore file is present, the directory loader will skip loading any files +// matching it. But .helmignore is not evaluated when reading out of an archive. +func Load(ctx context.Context, name string) (*chart.Chart, error) { + l, err := Loader(name) + if err != nil { + return nil, err + } + + return l.Load(ctx) +} + +// LoadFiles loads from in-memory files. +func LoadFiles(ctx context.Context, files []*archive.BufferedFile) (*chart.Chart, error) { + helmOpts := nelmcommon.HelmOptionsFromContext(ctx) + applyWerfExtensions := nelmcommon.HasHelmOptions(ctx) + + c := new(chart.Chart) + subcharts := make(map[string][]*archive.BufferedFile) + + if applyWerfExtensions { + c.SecretsRuntimeData = legacysecret.NewSecretsRuntimeData() + } + + // do not rely on assumed ordering of files in the chart and crash + // if Chart.yaml was not coming early enough to initialize metadata + for _, f := range files { + c.Raw = append(c.Raw, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + if f.Name == "Chart.yaml" { + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { + return c, fmt.Errorf("cannot load Chart.yaml: %w", err) + } + // NOTE(bacongobbler): while the chart specification says that APIVersion must be set, + // Helm 2 accepted charts that did not provide an APIVersion in their chart metadata. + // Because of that, if APIVersion is unset, we should assume we're loading a v1 chart. + if c.Metadata.APIVersion == "" { + c.Metadata.APIVersion = chart.APIVersionV1 + } + c.ModTime = f.ModTime + } + } + for _, f := range files { + switch { + case f.Name == "Chart.yaml": + // already processed + continue + case f.Name == "Chart.lock": + c.Lock = new(chart.Lock) + if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { + return c, fmt.Errorf("cannot load Chart.lock: %w", err) + } + case f.Name == "values.yaml": + values, err := LoadValues(bytes.NewReader(f.Data)) + if err != nil { + return c, fmt.Errorf("cannot load values.yaml: %w", err) + } + c.Values = values + case f.Name == "values.schema.json": + c.Schema = f.Data + c.SchemaModTime = f.ModTime + + // Deprecated: requirements.yaml is deprecated use Chart.yaml. + // We will handle it for you because we are nice people + case f.Name == "requirements.yaml": + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if c.Metadata.APIVersion != chart.APIVersionV1 { + log.Printf("Warning: Dependencies are handled in Chart.yaml since apiVersion \"v2\". We recommend migrating dependencies to Chart.yaml.") + } + if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil { + return c, fmt.Errorf("cannot load requirements.yaml: %w", err) + } + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Files = append(c.Files, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + } + // Deprecated: requirements.lock is deprecated use Chart.lock. + case f.Name == "requirements.lock": + c.Lock = new(chart.Lock) + if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { + return c, fmt.Errorf("cannot load requirements.lock: %w", err) + } + if c.Metadata == nil { + c.Metadata = new(chart.Metadata) + } + if c.Metadata.APIVersion != chart.APIVersionV1 { + log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.") + } + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Files = append(c.Files, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + } + + case strings.HasPrefix(f.Name, "templates/"): + c.Templates = append(c.Templates, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + case strings.HasPrefix(f.Name, "charts/"): + if filepath.Ext(f.Name) == ".prov" { + c.Files = append(c.Files, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + continue + } + + fname := strings.TrimPrefix(f.Name, "charts/") + cname := strings.SplitN(fname, "/", 2)[0] + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data}) + case applyWerfExtensions && strings.HasPrefix(f.Name, "ts/") && !strings.HasPrefix(f.Name, "ts/node_modules/"): + c.RuntimeFiles = append(c.RuntimeFiles, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + default: + c.Files = append(c.Files, &chartcommon.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) + } + } + + if applyWerfExtensions { + switch helmOpts.ChartLoadOpts.ChartType { + case nelmcommon.LegacyChartTypeBundle: + c.ExtraValues = helmOpts.ChartLoadOpts.ExtraValues + + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + CustomSecretValueFiles: helmOpts.ChartLoadOpts.SecretValuesFiles, + LoadFromLocalFilesystem: true, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + + if helmOpts.ChartLoadOpts.DefaultValuesDisable { + c.Values = nil + } + case nelmcommon.LegacyChartTypeChart: + c.ExtraValues = helmOpts.ChartLoadOpts.ExtraValues + + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + CustomSecretValueFiles: helmOpts.ChartLoadOpts.SecretValuesFiles, + LoadFromLocalFilesystem: nelmcommon.ChartFileReader == nil, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + + c.Metadata = autosetChartMetadata( + c.Metadata, + autosetChartMetadataOptions{ + DefaultAPIVersion: helmOpts.ChartLoadOpts.DefaultChartAPIVersion, + DefaultName: helmOpts.ChartLoadOpts.DefaultChartName, + DefaultVersion: helmOpts.ChartLoadOpts.DefaultChartVersion, + OverrideAppVersion: helmOpts.ChartLoadOpts.ChartAppVersion, + }, + ) + + c.Templates = append(c.Templates, &chartcommon.File{Name: "templates/_werf_helpers.tpl"}) + + if helmOpts.ChartLoadOpts.DefaultValuesDisable { + c.Values = nil + } + case nelmcommon.LegacyChartTypeSubchart: + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + LoadFromLocalFilesystem: nelmcommon.ChartFileReader == nil, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + case nelmcommon.LegacyChartTypeChartStub: + if !helmOpts.ChartLoadOpts.NoSecrets { + if err := c.SecretsRuntimeData.DecodeAndLoadSecrets( + ctx, + convertBufferedFiles(files), + secrets_manager.Manager, + chartcommon.DecodeAndLoadSecretsOptions{ + LoadFromLocalFilesystem: true, + NoDecryptSecrets: helmOpts.ChartLoadOpts.SecretKeyIgnore, + SecretsWorkingDir: helmOpts.ChartLoadOpts.SecretWorkDir, + WithoutDefaultSecretValues: helmOpts.ChartLoadOpts.DefaultSecretValuesDisable, + }, + ); err != nil { + return nil, fmt.Errorf("error decoding secrets: %w", err) + } + } + + c.Metadata = autosetChartMetadata( + c.Metadata, + autosetChartMetadataOptions{ + DefaultAPIVersion: chart.APIVersionV2, + DefaultName: "stubchartname", + DefaultVersion: "1.0.0", + }, + ) + + c.Templates = append(c.Templates, &chartcommon.File{Name: "templates/_werf_helpers.tpl"}) + default: + panic("unexpected type") + } + } + + if c.Metadata == nil { + return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck + } + + if err := c.Validate(); err != nil { + return c, err + } + + helmOpts.ChartLoadOpts.ChartType = nelmcommon.LegacyChartTypeSubchart + ctx = nelmcommon.ContextWithHelmOptions(ctx, helmOpts) + + for n, files := range subcharts { + var sc *chart.Chart + var err error + switch { + case strings.IndexAny(n, "_.") == 0: + continue + case filepath.Ext(n) == ".tgz": + file := files[0] + if file.Name != n { + return c, fmt.Errorf("error unpacking subchart tar in %s: expected %s, got %s", c.Name(), n, file.Name) + } + sc, err = LoadArchive(ctx, bytes.NewBuffer(file.Data)) + default: + buff := make([]*archive.BufferedFile, 0, len(files)) + for _, f := range files { + parts := strings.SplitN(f.Name, "/", 2) + if len(parts) < 2 { + continue + } + f.Name = parts[1] + buff = append(buff, f) + } + sc, err = LoadFiles(ctx, buff) + } + + if err != nil { + return c, fmt.Errorf("error unpacking subchart %s in %s: %w", n, c.Name(), err) + } + c.AddDependency(sc) + } + + return c, nil +} + +func convertBufferedFiles(files []*archive.BufferedFile) []*nelmcommon.BufferedFile { + var res []*nelmcommon.BufferedFile + for _, f := range files { + res = append(res, &nelmcommon.BufferedFile{Name: f.Name, Data: f.Data}) + } + + return res +} + +// LoadValues loads values from a reader. +// +// The reader is expected to contain one or more YAML documents, the values of which are merged. +// And the values can be either a chart's default values or user-supplied values. +func LoadValues(data io.Reader) (map[string]interface{}, error) { + values := map[string]interface{}{} + reader := utilyaml.NewYAMLReader(bufio.NewReader(data)) + for { + currentMap := map[string]interface{}{} + raw, err := reader.Read() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, fmt.Errorf("error reading yaml document: %w", err) + } + if err := yaml.Unmarshal(raw, ¤tMap); err != nil { + return nil, fmt.Errorf("cannot unmarshal yaml document: %w", err) + } + values = MergeMaps(values, currentMap) + } + return values, nil +} + +// MergeMaps merges two maps. If a key exists in both maps, the value from b will be used. +// If the value is a map, the maps will be merged recursively. +func MergeMaps(a, b map[string]interface{}) map[string]interface{} { + out := make(map[string]interface{}, len(a)) + maps.Copy(out, a) + for k, v := range b { + if v, ok := v.(map[string]interface{}); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]interface{}); ok { + out[k] = MergeMaps(bv, v) + continue + } + } + } + out[k] = v + } + return out +} diff --git a/pkg/helm/pkg/chart/v2/loader/load_test.go b/pkg/helm/pkg/chart/v2/loader/load_test.go new file mode 100644 index 00000000..b7c38851 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/load_test.go @@ -0,0 +1,780 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "context" + "errors" + "io" + "log" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func TestLoadDir(t *testing.T) { + l, err := Loader("testdata/frobnitz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadDirWithDevNull(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("test only works on unix systems with /dev/null present") + } + + l, err := Loader("testdata/frobnitz_with_dev_null") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + if _, err := l.Load(context.Background()); err == nil { + t.Errorf("packages with an irregular file (/dev/null) should not load") + } +} + +func TestLoadDirWithSymlink(t *testing.T) { + sym := filepath.Join("..", "LICENSE") + link := filepath.Join("testdata", "frobnitz_with_symlink", "LICENSE") + + if err := os.Symlink(sym, link); err != nil { + t.Fatal(err) + } + + defer os.Remove(link) + + l, err := Loader("testdata/frobnitz_with_symlink") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestBomTestData(t *testing.T) { + testFiles := []string{"frobnitz_with_bom/.helmignore", "frobnitz_with_bom/templates/template.tpl", "frobnitz_with_bom/Chart.yaml"} + for _, file := range testFiles { + data, err := os.ReadFile("testdata/" + file) + if err != nil || !bytes.HasPrefix(data, utf8bom) { + t.Errorf("Test file has no BOM or is invalid: testdata/%s", file) + } + } + + archive, err := os.ReadFile("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + unzipped, err := gzip.NewReader(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + defer unzipped.Close() + for _, testFile := range testFiles { + data := make([]byte, 3) + err := unzipped.Reset(bytes.NewReader(archive)) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + tr := tar.NewReader(unzipped) + for { + file, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } + if file != nil && strings.EqualFold(file.Name, testFile) { + _, err := tr.Read(data) + if err != nil { + t.Fatalf("Error reading archive frobnitz_with_bom.tgz: %s", err) + } else { + break + } + } + } + if !bytes.Equal(data, utf8bom) { + t.Fatalf("Test file has no BOM or is invalid: frobnitz_with_bom.tgz/%s", testFile) + } + } +} + +func TestLoadDirWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadArchiveWithUTFBOM(t *testing.T) { + l, err := Loader("testdata/frobnitz_with_bom.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) + verifyDependenciesLock(t, c) + verifyBomStripped(t, c.Files) +} + +func TestLoadV1(t *testing.T) { + l, err := Loader("testdata/frobnitz.v1") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadFileV1(t *testing.T) { + l, err := Loader("testdata/frobnitz.v1.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadFile(t *testing.T) { + l, err := Loader("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyFrobnitz(t, c) + verifyChart(t, c) + verifyDependencies(t, c) +} + +func TestLoadFiles_BadCases(t *testing.T) { + for _, tt := range []struct { + name string + bufferedFiles []*archive.BufferedFile + expectError string + }{ + { + name: "These files contain only requirements.lock", + bufferedFiles: []*archive.BufferedFile{ + { + Name: "requirements.lock", + ModTime: time.Now(), + Data: []byte(""), + }, + }, + expectError: "validation: chart.metadata.apiVersion is required"}, + } { + _, err := LoadFiles(context.Background(), tt.bufferedFiles) + if err == nil { + t.Fatal("expected error when load illegal files") + } + if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.name) + } + } +} + +func TestLoadFiles(t *testing.T) { + modTime := time.Now() + goodFiles := []*archive.BufferedFile{ + { + Name: "Chart.yaml", + ModTime: modTime, + Data: []byte(`apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +`), + }, + { + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), + }, + { + Name: "values.schema.json", + ModTime: modTime, + Data: []byte("type: Values"), + }, + { + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), + }, + } + + c, err := LoadFiles(context.Background(), goodFiles) + if err != nil { + t.Errorf("Expected good files to be loaded, got %v", err) + } + + if c.Name() != "frobnitz" { + t.Errorf("Expected chart name to be 'frobnitz', got %s", c.Name()) + } + + if c.Values["var"] != "some values" { + t.Error("Expected chart values to be populated with default values") + } + + if len(c.Raw) != 5 { + t.Errorf("Expected %d files, got %d", 5, len(c.Raw)) + } + + if !bytes.Equal(c.Schema, []byte("type: Values")) { + t.Error("Expected chart schema to be populated with default values") + } + + if len(c.Templates) != 2 { + t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) + } + + if _, err = LoadFiles(context.Background(), []*archive.BufferedFile{}); err == nil { + t.Fatal("Expected err to be non-nil") + } + if err.Error() != "Chart.yaml file is missing" { + t.Errorf("Expected chart metadata missing error, got '%s'", err.Error()) + } +} + +// Test the order of file loading. The Chart.yaml file needs to come first for +// later comparison checks. See https://github.com/helm/helm/pull/8948 +func TestLoadFilesOrder(t *testing.T) { + modTime := time.Now() + goodFiles := []*archive.BufferedFile{ + { + Name: "requirements.yaml", + ModTime: modTime, + Data: []byte("dependencies:"), + }, + { + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), + }, + + { + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), + }, + { + Name: "Chart.yaml", + ModTime: modTime, + Data: []byte(`apiVersion: v1 +name: frobnitz +description: This is a frobnitz. +version: "1.2.3" +keywords: + - frobnitz + - sprocket + - dodad +maintainers: + - name: The Helm Team + email: helm@example.com + - name: Someone Else + email: nobody@example.com +sources: + - https://example.com/foo/bar +home: http://example.com +icon: https://example.com/64x64.png +`), + }, + } + + // Capture stderr to make sure message about Chart.yaml handle dependencies + // is not present + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("Unable to create pipe: %s", err) + } + stderr := log.Writer() + log.SetOutput(w) + defer func() { + log.SetOutput(stderr) + }() + + _, err = LoadFiles(context.Background(), goodFiles) + if err != nil { + t.Errorf("Expected good files to be loaded, got %v", err) + } + w.Close() + + var text bytes.Buffer + io.Copy(&text, r) + if text.String() != "" { + t.Errorf("Expected no message to Stderr, got %s", text.String()) + } + +} + +// Packaging the chart on a Windows machine will produce an +// archive that has \\ as delimiters. Test that we support these archives +func TestLoadFileBackslash(t *testing.T) { + c, err := Load(context.Background(), "testdata/frobnitz_backslash-1.2.3.tgz") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyChartFileAndTemplate(t, c, "frobnitz_backslash") + verifyChart(t, c) + verifyDependencies(t, c) +} + +func TestLoadV2WithReqs(t *testing.T) { + l, err := Loader("testdata/frobnitz.v2.reqs") + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + c, err := l.Load(context.Background()) + if err != nil { + t.Fatalf("Failed to load testdata: %s", err) + } + verifyDependencies(t, c) + verifyDependenciesLock(t, c) +} + +func TestLoadInvalidArchive(t *testing.T) { + tmpdir := t.TempDir() + + writeTar := func(filename, internalPath string, body []byte) { + dest, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + zipper := gzip.NewWriter(dest) + tw := tar.NewWriter(zipper) + + h := &tar.Header{ + Name: internalPath, + Mode: 0755, + Size: int64(len(body)), + ModTime: time.Now(), + } + if err := tw.WriteHeader(h); err != nil { + t.Fatal(err) + } + if _, err := tw.Write(body); err != nil { + t.Fatal(err) + } + tw.Close() + zipper.Close() + dest.Close() + } + + for _, tt := range []struct { + chartname string + internal string + expectError string + }{ + {"illegal-dots.tgz", "../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots2.tgz", "/foo/../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots3.tgz", "/../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-dots4.tgz", "./../../malformed-helm-test", "chart illegally references parent directory"}, + {"illegal-name.tgz", "./.", "chart illegally contains content outside the base directory"}, + {"illegal-name2.tgz", "/./.", "chart illegally contains content outside the base directory"}, + {"illegal-name3.tgz", "missing-leading-slash", "chart illegally contains content outside the base directory"}, + {"illegal-name4.tgz", "/missing-leading-slash", "Chart.yaml file is missing"}, + {"illegal-abspath.tgz", "//foo", "chart illegally contains absolute paths"}, + {"illegal-abspath2.tgz", "///foo", "chart illegally contains absolute paths"}, + {"illegal-abspath3.tgz", "\\\\foo", "chart illegally contains absolute paths"}, + {"illegal-abspath3.tgz", "\\..\\..\\foo", "chart illegally references parent directory"}, + + // Under special circumstances, this can get normalized to things that look like absolute Windows paths + {"illegal-abspath4.tgz", "\\.\\c:\\\\foo", "chart contains illegally named files"}, + {"illegal-abspath5.tgz", "/./c://foo", "chart contains illegally named files"}, + {"illegal-abspath6.tgz", "\\\\?\\Some\\windows\\magic", "chart illegally contains absolute paths"}, + } { + illegalChart := filepath.Join(tmpdir, tt.chartname) + writeTar(illegalChart, tt.internal, []byte("hello: world")) + _, err := Load(context.Background(), illegalChart) + if err == nil { + t.Fatal("expected error when unpacking illegal files") + } + if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("Expected error to contain %q, got %q for %s", tt.expectError, err.Error(), tt.chartname) + } + } + + // Make sure that absolute path gets interpreted as relative + illegalChart := filepath.Join(tmpdir, "abs-path.tgz") + writeTar(illegalChart, "/Chart.yaml", []byte("hello: world")) + _, err := Load(context.Background(), illegalChart) + if err.Error() != "validation: chart.metadata.name is required" { + t.Error(err) + } + + // And just to validate that the above was not spurious + illegalChart = filepath.Join(tmpdir, "abs-path2.tgz") + writeTar(illegalChart, "files/whatever.yaml", []byte("hello: world")) + _, err = Load(context.Background(), illegalChart) + if err.Error() != "Chart.yaml file is missing" { + t.Errorf("Unexpected error message: %s", err) + } + + // Finally, test that drive letter gets stripped off on Windows + illegalChart = filepath.Join(tmpdir, "abs-winpath.tgz") + writeTar(illegalChart, "c:\\Chart.yaml", []byte("hello: world")) + _, err = Load(context.Background(), illegalChart) + if err.Error() != "validation: chart.metadata.name is required" { + t.Error(err) + } +} + +func TestLoadValues(t *testing.T) { + testCases := map[string]struct { + data []byte + expctedValues map[string]interface{} + }{ + "It should load values correctly": { + data: []byte(` +foo: + image: foo:v1 +bar: + version: v2 +`), + expctedValues: map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "foo:v1", + }, + "bar": map[string]interface{}{ + "version": "v2", + }, + }, + }, + "It should load values correctly with multiple documents in one file": { + data: []byte(` +foo: + image: foo:v1 +bar: + version: v2 +--- +foo: + image: foo:v2 +`), + expctedValues: map[string]interface{}{ + "foo": map[string]interface{}{ + "image": "foo:v2", + }, + "bar": map[string]interface{}{ + "version": "v2", + }, + }, + }, + } + for testName, testCase := range testCases { + t.Run(testName, func(tt *testing.T) { + values, err := LoadValues(bytes.NewReader(testCase.data)) + if err != nil { + tt.Fatal(err) + } + if !reflect.DeepEqual(values, testCase.expctedValues) { + tt.Errorf("Expected values: %v, got %v", testCase.expctedValues, values) + } + }) + } +} + +func TestMergeValuesV2(t *testing.T) { + nestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "stuff", + }, + } + anotherNestedMap := map[string]interface{}{ + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + flatMap := map[string]interface{}{ + "foo": "bar", + "baz": "stuff", + } + anotherFlatMap := map[string]interface{}{ + "testing": "fun", + } + + testMap := MergeMaps(flatMap, nestedMap) + equal := reflect.DeepEqual(testMap, nestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) + } + + testMap = MergeMaps(nestedMap, flatMap) + equal = reflect.DeepEqual(testMap, flatMap) + if !equal { + t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) + } + + testMap = MergeMaps(nestedMap, anotherNestedMap) + equal = reflect.DeepEqual(testMap, anotherNestedMap) + if !equal { + t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) + } + + testMap = MergeMaps(anotherFlatMap, anotherNestedMap) + expectedMap := map[string]interface{}{ + "testing": "fun", + "foo": "bar", + "baz": map[string]string{ + "cool": "things", + "awesome": "stuff", + }, + } + equal = reflect.DeepEqual(testMap, expectedMap) + if !equal { + t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) + } +} + +func verifyChart(t *testing.T, c *chart.Chart) { + t.Helper() + if c.Name() == "" { + t.Fatalf("No chart metadata found on %v", c) + } + t.Logf("Verifying chart %s", c.Name()) + if len(c.Templates) != 1 { + t.Errorf("Expected 1 template, got %d", len(c.Templates)) + } + + numfiles := 6 + if len(c.Files) != numfiles { + t.Errorf("Expected %d extra files, got %d", numfiles, len(c.Files)) + for _, n := range c.Files { + t.Logf("\t%s", n.Name) + } + } + + if len(c.Dependencies()) != 2 { + t.Errorf("Expected 2 dependencies, got %d (%v)", len(c.Dependencies()), c.Dependencies()) + for _, d := range c.Dependencies() { + t.Logf("\tSubchart: %s\n", d.Name()) + } + } + + expect := map[string]map[string]string{ + "alpine": { + "version": "0.1.0", + }, + "mariner": { + "version": "4.3.2", + }, + } + + for _, dep := range c.Dependencies() { + if dep.Metadata == nil { + t.Fatalf("expected metadata on dependency: %v", dep) + } + exp, ok := expect[dep.Name()] + if !ok { + t.Fatalf("Unknown dependency %s", dep.Name()) + } + if exp["version"] != dep.Metadata.Version { + t.Errorf("Expected %s version %s, got %s", dep.Name(), exp["version"], dep.Metadata.Version) + } + } + +} + +func verifyDependencies(t *testing.T, c *chart.Chart) { + t.Helper() + if len(c.Metadata.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) + } + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := c.Metadata.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + +func verifyDependenciesLock(t *testing.T, c *chart.Chart) { + t.Helper() + if len(c.Metadata.Dependencies) != 2 { + t.Errorf("Expected 2 dependencies, got %d", len(c.Metadata.Dependencies)) + } + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + for i, tt := range tests { + d := c.Metadata.Dependencies[i] + if d.Name != tt.Name { + t.Errorf("Expected dependency named %q, got %q", tt.Name, d.Name) + } + if d.Version != tt.Version { + t.Errorf("Expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, d.Version) + } + if d.Repository != tt.Repository { + t.Errorf("Expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, d.Repository) + } + } +} + +func verifyFrobnitz(t *testing.T, c *chart.Chart) { + t.Helper() + verifyChartFileAndTemplate(t, c, "frobnitz") +} + +func verifyChartFileAndTemplate(t *testing.T, c *chart.Chart, name string) { + t.Helper() + if c.Metadata == nil { + t.Fatal("Metadata is nil") + } + if c.Name() != name { + t.Errorf("Expected %s, got %s", name, c.Name()) + } + if len(c.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(c.Templates)) + } + if c.Templates[0].Name != "templates/template.tpl" { + t.Errorf("Unexpected template: %s", c.Templates[0].Name) + } + if len(c.Templates[0].Data) == 0 { + t.Error("No template data.") + } + if len(c.Files) != 6 { + t.Fatalf("Expected 6 Files, got %d", len(c.Files)) + } + if len(c.Dependencies()) != 2 { + t.Fatalf("Expected 2 Dependency, got %d", len(c.Dependencies())) + } + if len(c.Metadata.Dependencies) != 2 { + t.Fatalf("Expected 2 Dependencies.Dependency, got %d", len(c.Metadata.Dependencies)) + } + if len(c.Lock.Dependencies) != 2 { + t.Fatalf("Expected 2 Lock.Dependency, got %d", len(c.Lock.Dependencies)) + } + + for _, dep := range c.Dependencies() { + switch dep.Name() { + case "mariner": + case "alpine": + if len(dep.Templates) != 1 { + t.Fatalf("Expected 1 template, got %d", len(dep.Templates)) + } + if dep.Templates[0].Name != "templates/alpine-pod.yaml" { + t.Errorf("Unexpected template: %s", dep.Templates[0].Name) + } + if len(dep.Templates[0].Data) == 0 { + t.Error("No template data.") + } + if len(dep.Files) != 1 { + t.Fatalf("Expected 1 Files, got %d", len(dep.Files)) + } + if len(dep.Dependencies()) != 2 { + t.Fatalf("Expected 2 Dependency, got %d", len(dep.Dependencies())) + } + default: + t.Errorf("Unexpected dependency %s", dep.Name()) + } + } +} + +func verifyBomStripped(t *testing.T, files []*common.File) { + t.Helper() + for _, file := range files { + if bytes.HasPrefix(file.Data, utf8bom) { + t.Errorf("Byte Order Mark still present in processed file %s", file.Name) + } + } +} diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/LICENSE similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/LICENSE rename to pkg/helm/pkg/chart/v2/loader/testdata/LICENSE diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/albatross/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/albatross/Chart.yaml new file mode 100644 index 00000000..eeef737f --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/albatross/Chart.yaml @@ -0,0 +1,4 @@ +name: albatross +description: A Helm chart for Kubernetes +version: 0.1.0 +home: "" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/albatross/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/albatross/values.yaml new file mode 100644 index 00000000..3121cd7c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/albatross/values.yaml @@ -0,0 +1,4 @@ +albatross: "true" + +global: + author: Coleridge diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz-1.2.3.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz-1.2.3.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz-1.2.3.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/.helmignore similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/.helmignore rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/.helmignore diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/Chart.lock b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/Chart.lock similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/Chart.lock rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/Chart.lock diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/INSTALL.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/INSTALL.txt rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/INSTALL.txt diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/README.md rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/_ignore_me similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/_ignore_me rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/_ignore_me diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/README.md rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/README.md diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/charts/mast1/values.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/charts/mast2-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/alpine/values.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/docs/README.md similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/docs/README.md rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/docs/README.md diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/icon.svg similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/icon.svg rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/icon.svg diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/ignore/me.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/ignore/me.txt rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/requirements.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/requirements.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v1/requirements.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/requirements.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/templates/template.tpl similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/templates/template.tpl rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/templates/template.tpl diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/values.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v1/values.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..21ae20aa --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/cmd/helm/testdata/output/lint-quiet.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/ignore/me.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/lint-quiet.txt rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/ignore/me.txt diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/requirements.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/requirements.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz.v2.reqs/requirements.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/requirements.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz.v2.reqs/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/Chart.lock b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/Chart.lock new file mode 100644 index 00000000..6fcc2ed9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..21ae20aa --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/ignore/me.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/ignore/me.txt diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash-1.2.3.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash-1.2.3.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash-1.2.3.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/.helmignore new file mode 100755 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/Chart.lock b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/Chart.lock new file mode 100755 index 00000000..6fcc2ed9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/INSTALL.txt new file mode 100755 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/LICENSE new file mode 100755 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/README.md new file mode 100755 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/_ignore_me new file mode 100755 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/README.md new file mode 100755 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml new file mode 100755 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100755 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml new file mode 100755 index 00000000..0ac5ca6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service | quote }} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml new file mode 100755 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/docs/README.md new file mode 100755 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/icon.svg new file mode 100755 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/cmd/helm/testdata/testcharts/object-order/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/ignore/me.txt old mode 100644 new mode 100755 similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/object-order/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/ignore/me.txt diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/templates/template.tpl new file mode 100755 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/values.yaml new file mode 100755 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_backslash/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/.helmignore new file mode 100644 index 00000000..7a4b92da --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/Chart.lock b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/Chart.lock new file mode 100644 index 00000000..ed43b227 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/INSTALL.txt new file mode 100644 index 00000000..77c4e724 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/LICENSE new file mode 100644 index 00000000..c27b00bf --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/README.md new file mode 100644 index 00000000..e9c40031 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/_ignore_me new file mode 100644 index 00000000..a7e3a38b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/README.md new file mode 100644 index 00000000..ea7526be --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..f690d53c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..f3e662a2 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml new file mode 100644 index 00000000..6b7cb259 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/docs/README.md new file mode 100644 index 00000000..816c3e43 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/ignore/me.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/ignore/me.txt diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/templates/template.tpl new file mode 100644 index 00000000..bb29c549 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/values.yaml new file mode 100644 index 00000000..c24ceadf --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_bom/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/Chart.lock b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/Chart.lock new file mode 100644 index 00000000..6fcc2ed9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/LICENSE b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..21ae20aa --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/lint/rules/testdata/withsubchart/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/ignore/me.txt similarity index 100% rename from pkg/helm/pkg/lint/rules/testdata/withsubchart/values.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/ignore/me.txt diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/null b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/null new file mode 120000 index 00000000..dc1dc0cd --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/null @@ -0,0 +1 @@ +/dev/null \ No newline at end of file diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_dev_null/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/.helmignore b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/Chart.lock b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/Chart.lock new file mode 100644 index 00000000..6fcc2ed9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/INSTALL.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/_ignore_me b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..21ae20aa --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + app.kubernetes.io/name: {{.Chart.Name}} + helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}" +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.9" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..3190136b Binary files /dev/null and b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/docs/README.md b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/icon.svg b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/ignore/me.txt b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/ignore/me.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/templates/template.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/frobnitz_with_symlink/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chartutil/testdata/genfrob.sh b/pkg/helm/pkg/chart/v2/loader/testdata/genfrob.sh similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/genfrob.sh rename to pkg/helm/pkg/chart/v2/loader/testdata/genfrob.sh diff --git a/pkg/helm/pkg/chart/loader/testdata/mariner/Chart.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/mariner/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/mariner/Chart.yaml rename to pkg/helm/pkg/chart/v2/loader/testdata/mariner/Chart.yaml diff --git a/pkg/helm/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz b/pkg/helm/pkg/chart/v2/loader/testdata/mariner/charts/albatross-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/chart/loader/testdata/mariner/charts/albatross-0.1.0.tgz rename to pkg/helm/pkg/chart/v2/loader/testdata/mariner/charts/albatross-0.1.0.tgz diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/mariner/templates/placeholder.tpl b/pkg/helm/pkg/chart/v2/loader/testdata/mariner/templates/placeholder.tpl new file mode 100644 index 00000000..29c11843 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/mariner/templates/placeholder.tpl @@ -0,0 +1 @@ +# This is a placeholder. diff --git a/pkg/helm/pkg/chart/v2/loader/testdata/mariner/values.yaml b/pkg/helm/pkg/chart/v2/loader/testdata/mariner/values.yaml new file mode 100644 index 00000000..b0ccb008 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/loader/testdata/mariner/values.yaml @@ -0,0 +1,7 @@ +# Default values for . +# This is a YAML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name: "value" + +: + test: true diff --git a/pkg/helm/pkg/chart/v2/metadata.go b/pkg/helm/pkg/chart/v2/metadata.go new file mode 100644 index 00000000..c4600786 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/metadata.go @@ -0,0 +1,178 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v2 + +import ( + "path/filepath" + "strings" + "unicode" + + "github.com/Masterminds/semver/v3" +) + +// Maintainer describes a Chart maintainer. +type Maintainer struct { + // Name is a user name or organization name + Name string `json:"name,omitempty"` + // Email is an optional email address to contact the named maintainer + Email string `json:"email,omitempty"` + // URL is an optional URL to an address for the named maintainer + URL string `json:"url,omitempty"` +} + +// Validate checks valid data and sanitizes string characters. +func (m *Maintainer) Validate() error { + if m == nil { + return ValidationError("maintainers must not contain empty or null nodes") + } + m.Name = sanitizeString(m.Name) + m.Email = sanitizeString(m.Email) + m.URL = sanitizeString(m.URL) + return nil +} + +// Metadata for a Chart file. This models the structure of a Chart.yaml file. +type Metadata struct { + // The name of the chart. Required. + Name string `json:"name,omitempty"` + // The URL to a relevant project page, git repo, or contact person + Home string `json:"home,omitempty"` + // Source is the URL to the source code of this chart + Sources []string `json:"sources,omitempty"` + // A version string of the chart. Required. + Version string `json:"version,omitempty"` + // A one-sentence description of the chart + Description string `json:"description,omitempty"` + // A list of string keywords + Keywords []string `json:"keywords,omitempty"` + // A list of name and URL/email address combinations for the maintainer(s) + Maintainers []*Maintainer `json:"maintainers,omitempty"` + // The URL to an icon file. + Icon string `json:"icon,omitempty"` + // The API Version of this chart. Required. + APIVersion string `json:"apiVersion,omitempty"` + // The condition to check to enable chart + Condition string `json:"condition,omitempty"` + // The tags to check to enable chart + Tags string `json:"tags,omitempty"` + // The version of the application enclosed inside of this chart. + AppVersion string `json:"appVersion,omitempty"` + // Whether or not this chart is deprecated + Deprecated bool `json:"deprecated,omitempty"` + // Annotations are additional mappings uninterpreted by Helm, + // made available for inspection by other applications. + Annotations map[string]string `json:"annotations,omitempty"` + // KubeVersion is a SemVer constraint specifying the version of Kubernetes required. + KubeVersion string `json:"kubeVersion,omitempty"` + // Dependencies are a list of dependencies for a chart. + Dependencies []*Dependency `json:"dependencies,omitempty"` + // Specifies the chart type: application or library + Type string `json:"type,omitempty"` +} + +// Validate checks the metadata for known issues and sanitizes string +// characters. +func (md *Metadata) Validate() error { + if md == nil { + return ValidationError("chart.metadata is required") + } + + md.Name = sanitizeString(md.Name) + md.Description = sanitizeString(md.Description) + md.Home = sanitizeString(md.Home) + md.Icon = sanitizeString(md.Icon) + md.Condition = sanitizeString(md.Condition) + md.Tags = sanitizeString(md.Tags) + md.AppVersion = sanitizeString(md.AppVersion) + md.KubeVersion = sanitizeString(md.KubeVersion) + for i := range md.Sources { + md.Sources[i] = sanitizeString(md.Sources[i]) + } + for i := range md.Keywords { + md.Keywords[i] = sanitizeString(md.Keywords[i]) + } + + if md.APIVersion == "" { + return ValidationError("chart.metadata.apiVersion is required") + } + if md.Name == "" { + return ValidationError("chart.metadata.name is required") + } + + if md.Name != filepath.Base(md.Name) { + return ValidationErrorf("chart.metadata.name %q is invalid", md.Name) + } + + if md.Version == "" { + return ValidationError("chart.metadata.version is required") + } + if !isValidSemver(md.Version) { + return ValidationErrorf("chart.metadata.version %q is invalid", md.Version) + } + if !isValidChartType(md.Type) { + return ValidationError("chart.metadata.type must be application or library") + } + + for _, m := range md.Maintainers { + if err := m.Validate(); err != nil { + return err + } + } + + // Aliases need to be validated here to make sure that the alias name does + // not contain any illegal characters. + dependencies := map[string]*Dependency{} + for _, dependency := range md.Dependencies { + if err := dependency.Validate(); err != nil { + return err + } + key := dependency.Name + if dependency.Alias != "" { + key = dependency.Alias + } + if dependencies[key] != nil { + return ValidationErrorf("more than one dependency with name or alias %q", key) + } + dependencies[key] = dependency + } + return nil +} + +func isValidChartType(in string) bool { + switch in { + case "", "application", "library": + return true + } + return false +} + +func isValidSemver(v string) bool { + _, err := semver.NewVersion(v) + return err == nil +} + +// sanitizeString normalize spaces and removes non-printable characters. +func sanitizeString(str string) string { + return strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return ' ' + } + if unicode.IsPrint(r) { + return r + } + return -1 + }, str) +} diff --git a/pkg/helm/pkg/chart/v2/metadata_test.go b/pkg/helm/pkg/chart/v2/metadata_test.go new file mode 100644 index 00000000..7892f020 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/metadata_test.go @@ -0,0 +1,201 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package v2 + +import ( + "testing" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + md *Metadata + err error + }{ + { + "chart without metadata", + nil, + ValidationError("chart.metadata is required"), + }, + { + "chart without apiVersion", + &Metadata{Name: "test", Version: "1.0"}, + ValidationError("chart.metadata.apiVersion is required"), + }, + { + "chart without name", + &Metadata{APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name is required"), + }, + { + "chart without name", + &Metadata{Name: "../../test", APIVersion: "v2", Version: "1.0"}, + ValidationError("chart.metadata.name \"../../test\" is invalid"), + }, + { + "chart without version", + &Metadata{Name: "test", APIVersion: "v2"}, + ValidationError("chart.metadata.version is required"), + }, + { + "chart with bad type", + &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "test"}, + ValidationError("chart.metadata.type must be application or library"), + }, + { + "chart without dependency", + &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"}, + nil, + }, + { + "dependency with valid alias", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "dependency", Alias: "legal-alias"}, + }, + }, + nil, + }, + { + "dependency with bad characters in alias", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "bad", Alias: "illegal alias"}, + }, + }, + ValidationError("dependency \"bad\" has disallowed characters in the alias"), + }, + { + "same dependency twice", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Alias: ""}, + {Name: "foo", Alias: ""}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + "two dependencies with alias from second dependency shadowing first one", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Alias: ""}, + {Name: "bar", Alias: "foo"}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + // this case would make sense and could work in future versions of Helm, currently template rendering would + // result in undefined behaviour + "same dependency twice with different version", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Alias: "", Version: "1.2.3"}, + {Name: "foo", Alias: "", Version: "1.0.0"}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + // this case would make sense and could work in future versions of Helm, currently template rendering would + // result in undefined behaviour + "two dependencies with same name but different repos", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + {Name: "foo", Repository: "repo-0"}, + {Name: "foo", Repository: "repo-1"}, + }, + }, + ValidationError("more than one dependency with name or alias \"foo\""), + }, + { + "dependencies has nil", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Dependencies: []*Dependency{ + nil, + }, + }, + ValidationError("dependencies must not contain empty or null nodes"), + }, + { + "maintainer not empty", + &Metadata{ + Name: "test", + APIVersion: "v2", + Version: "1.0", + Type: "application", + Maintainers: []*Maintainer{ + nil, + }, + }, + ValidationError("maintainers must not contain empty or null nodes"), + }, + { + "version invalid", + &Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"}, + ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"), + }, + } + + for _, tt := range tests { + result := tt.md.Validate() + if result != tt.err { + t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name) + } + } +} + +func TestValidate_sanitize(t *testing.T) { + md := &Metadata{APIVersion: "v2", Name: "test", Version: "1.0", Description: "\adescr\u0081iption\rtest", Maintainers: []*Maintainer{{Name: "\r"}}} + if err := md.Validate(); err != nil { + t.Fatalf("unexpected error: %s", err) + } + if md.Description != "description test" { + t.Fatalf("description was not sanitized: %q", md.Description) + } + if md.Maintainers[0].Name != " " { + t.Fatal("maintainer name was not sanitized") + } +} diff --git a/pkg/helm/pkg/chart/v2/util/chartfile.go b/pkg/helm/pkg/chart/v2/util/chartfile.go new file mode 100644 index 00000000..954e9762 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/chartfile.go @@ -0,0 +1,105 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +// LoadChartfile loads a Chart.yaml file into a *chart.Metadata. +func LoadChartfile(filename string) (*chart.Metadata, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := new(chart.Metadata) + err = yaml.Unmarshal(b, y) + return y, err +} + +// StrictLoadChartfile loads a Chart.yaml into a *chart.Metadata using a strict unmarshaling +func StrictLoadChartfile(filename string) (*chart.Metadata, error) { + b, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + y := new(chart.Metadata) + err = yaml.UnmarshalStrict(b, y) + return y, err +} + +// SaveChartfile saves the given metadata as a Chart.yaml file at the given path. +// +// 'filename' should be the complete path and filename ('foo/Chart.yaml') +func SaveChartfile(filename string, cf *chart.Metadata) error { + // Pull out the dependencies of a v1 Chart, since there's no way + // to tell the serializer to skip a field for just this use case + savedDependencies := cf.Dependencies + if cf.APIVersion == chart.APIVersionV1 { + cf.Dependencies = nil + } + out, err := yaml.Marshal(cf) + if cf.APIVersion == chart.APIVersionV1 { + cf.Dependencies = savedDependencies + } + if err != nil { + return err + } + return os.WriteFile(filename, out, 0644) +} + +// IsChartDir validate a chart directory. +// +// Checks for a valid Chart.yaml. +func IsChartDir(dirName string) (bool, error) { + if fi, err := os.Stat(dirName); err != nil { + return false, err + } else if !fi.IsDir() { + return false, fmt.Errorf("%q is not a directory", dirName) + } + + chartYaml := filepath.Join(dirName, ChartfileName) + if _, err := os.Stat(chartYaml); errors.Is(err, fs.ErrNotExist) { + return false, fmt.Errorf("no %s exists in directory %q", ChartfileName, dirName) + } + + chartYamlContent, err := os.ReadFile(chartYaml) + if err != nil { + return false, fmt.Errorf("cannot read %s in directory %q", ChartfileName, dirName) + } + + chartContent := new(chart.Metadata) + if err := yaml.Unmarshal(chartYamlContent, &chartContent); err != nil { + return false, err + } + if chartContent == nil { + return false, fmt.Errorf("chart metadata (%s) missing", ChartfileName) + } + if chartContent.Name == "" { + return false, fmt.Errorf("invalid chart (%s): name must not be empty", ChartfileName) + } + + return true, nil +} diff --git a/pkg/helm/pkg/chart/v2/util/chartfile_test.go b/pkg/helm/pkg/chart/v2/util/chartfile_test.go new file mode 100644 index 00000000..e7c4790d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/chartfile_test.go @@ -0,0 +1,121 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "testing" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +const testfile = "testdata/chartfiletest.yaml" + +func TestLoadChartfile(t *testing.T) { + f, err := LoadChartfile(testfile) + if err != nil { + t.Errorf("Failed to open %s: %s", testfile, err) + return + } + verifyChartfile(t, f, "frobnitz") +} + +func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { + t.Helper() + if f == nil { //nolint:staticcheck + t.Fatal("Failed verifyChartfile because f is nil") + } + + if f.APIVersion != chart.APIVersionV1 { //nolint:staticcheck + t.Errorf("Expected API Version %q, got %q", chart.APIVersionV1, f.APIVersion) + } + + if f.Name != name { + t.Errorf("Expected %s, got %s", name, f.Name) + } + + if f.Description != "This is a frobnitz." { + t.Errorf("Unexpected description %q", f.Description) + } + + if f.Version != "1.2.3" { + t.Errorf("Unexpected version %q", f.Version) + } + + if len(f.Maintainers) != 2 { + t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) + } + + if f.Maintainers[0].Name != "The Helm Team" { + t.Errorf("Unexpected maintainer name.") + } + + if f.Maintainers[1].Email != "nobody@example.com" { + t.Errorf("Unexpected maintainer email.") + } + + if len(f.Sources) != 1 { + t.Fatalf("Unexpected number of sources") + } + + if f.Sources[0] != "https://example.com/foo/bar" { + t.Errorf("Expected https://example.com/foo/bar, got %s", f.Sources) + } + + if f.Home != "http://example.com" { + t.Error("Unexpected home.") + } + + if f.Icon != "https://example.com/64x64.png" { + t.Errorf("Unexpected icon: %q", f.Icon) + } + + if len(f.Keywords) != 3 { + t.Error("Unexpected keywords") + } + + if len(f.Annotations) != 2 { + t.Fatalf("Unexpected annotations") + } + + if want, got := "extravalue", f.Annotations["extrakey"]; want != got { + t.Errorf("Want %q, but got %q", want, got) + } + + if want, got := "anothervalue", f.Annotations["anotherkey"]; want != got { + t.Errorf("Want %q, but got %q", want, got) + } + + kk := []string{"frobnitz", "sprocket", "dodad"} + for i, k := range f.Keywords { + if kk[i] != k { + t.Errorf("Expected %q, got %q", kk[i], k) + } + } +} + +func TestIsChartDir(t *testing.T) { + validChartDir, err := IsChartDir("testdata/frobnitz") + if !validChartDir { + t.Errorf("unexpected error while reading chart-directory: (%v)", err) + return + } + validChartDir, err = IsChartDir("testdata") + if validChartDir || err == nil { + t.Errorf("expected error but did not get any") + return + } +} diff --git a/pkg/helm/pkg/chart/v2/util/compatible.go b/pkg/helm/pkg/chart/v2/util/compatible.go new file mode 100644 index 00000000..d384d2d4 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/compatible.go @@ -0,0 +1,34 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import "github.com/Masterminds/semver/v3" + +// IsCompatibleRange compares a version to a constraint. +// It returns true if the version matches the constraint, and false in all other cases. +func IsCompatibleRange(constraint, ver string) bool { + sv, err := semver.NewVersion(ver) + if err != nil { + return false + } + + c, err := semver.NewConstraint(constraint) + if err != nil { + return false + } + return c.Check(sv) +} diff --git a/pkg/helm/pkg/chart/v2/util/compatible_test.go b/pkg/helm/pkg/chart/v2/util/compatible_test.go new file mode 100644 index 00000000..e17d33e3 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/compatible_test.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package version represents the current version of the project. +package util + +import "testing" + +func TestIsCompatibleRange(t *testing.T) { + tests := []struct { + constraint string + ver string + expected bool + }{ + {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, + {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, + {"v2.0.0", "v2.0.0-alpha.4", false}, + {"v2.0.0-alpha.4", "v2.0.0", false}, + {"~v2.0.0", "v2.0.1", true}, + {"v2", "v2.0.0", true}, + {">2.0.0", "v2.1.1", true}, + {"v2.1.*", "v2.1.1", true}, + } + + for _, tt := range tests { + if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected { + t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver) + } + } +} diff --git a/pkg/helm/pkg/chart/v2/util/create.go b/pkg/helm/pkg/chart/v2/util/create.go new file mode 100644 index 00000000..a850b3a9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/create.go @@ -0,0 +1,834 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" +) + +// chartName is a regular expression for testing the supplied name of a chart. +// This regular expression is probably stricter than it needs to be. We can relax it +// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be +// problematic. +var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$") + +const ( + // ChartfileName is the default Chart file name. + ChartfileName = "Chart.yaml" + // ValuesfileName is the default values file name. + ValuesfileName = "values.yaml" + // SchemafileName is the default values schema file name. + SchemafileName = "values.schema.json" + // TemplatesDir is the relative directory name for templates. + TemplatesDir = "templates" + // ChartsDir is the relative directory name for charts dependencies. + ChartsDir = "charts" + // TemplatesTestsDir is the relative directory name for tests. + TemplatesTestsDir = TemplatesDir + sep + "tests" + // IgnorefileName is the name of the Helm ignore file. + IgnorefileName = ".helmignore" + // IngressFileName is the name of the example ingress file. + IngressFileName = TemplatesDir + sep + "ingress.yaml" + // HTTPRouteFileName is the name of the example HTTPRoute file. + HTTPRouteFileName = TemplatesDir + sep + "httproute.yaml" + // DeploymentName is the name of the example deployment file. + DeploymentName = TemplatesDir + sep + "deployment.yaml" + // ServiceName is the name of the example service file. + ServiceName = TemplatesDir + sep + "service.yaml" + // ServiceAccountName is the name of the example serviceaccount file. + ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml" + // HorizontalPodAutoscalerName is the name of the example hpa file. + HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml" + // NotesName is the name of the example NOTES.txt file. + NotesName = TemplatesDir + sep + "NOTES.txt" + // HelpersName is the name of the example helpers file. + HelpersName = TemplatesDir + sep + "_helpers.tpl" + // TestConnectionName is the name of the example test file. + TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml" +) + +// maxChartNameLength is lower than the limits we know of with certain file systems, +// and with certain Kubernetes fields. +const maxChartNameLength = 250 + +const sep = string(filepath.Separator) + +const defaultChartfile = `apiVersion: v2 +name: %s +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" +` + +const defaultValues = `# Default values for %s. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created. + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account. + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template. + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} +` + +const defaultIgnore = `# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +` + +const defaultIngress = `{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include ".fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} +` + +const defaultHTTPRoute = `{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include ".fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} +` + +const defaultDeployment = `apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include ".selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include ".labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include ".serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +` + +const defaultService = `apiVersion: v1 +kind: Service +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include ".selectorLabels" . | nindent 4 }} +` + +const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include ".serviceAccountName" . }} + labels: + {{- include ".labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} +` + +const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include ".fullname" . }} + labels: + {{- include ".labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include ".fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} +` + +const defaultNotes = `1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include ".fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include ".fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include ".fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include ".name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} +` + +const defaultHelpers = `{{/* +Expand the name of the chart. +*/}} +{{- define ".name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define ".fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define ".chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define ".labels" -}} +helm.sh/chart: {{ include ".chart" . }} +{{ include ".selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define ".selectorLabels" -}} +app.kubernetes.io/name: {{ include ".name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define ".serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include ".fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} +` + +const defaultTestConnection = `apiVersion: v1 +kind: Pod +metadata: + name: "{{ include ".fullname" . }}-test-connection" + labels: + {{- include ".labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include ".fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never +` + +// Stderr is an io.Writer to which error messages can be written +// +// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward +// compatibility. +var Stderr io.Writer = os.Stderr + +// CreateFrom creates a new chart, but scaffolds it from the src chart. +func CreateFrom(chartfile *chart.Metadata, dest, src string) error { + schart, err := loader.Load(context.Background(), src) + if err != nil { + return fmt.Errorf("could not load %s: %w", src, err) + } + + schart.Metadata = chartfile + + var updatedTemplates []*common.File + + for _, template := range schart.Templates { + newData := transform(string(template.Data), schart.Name()) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData}) + } + + schart.Templates = updatedTemplates + b, err := yaml.Marshal(schart.Values) + if err != nil { + return fmt.Errorf("reading values file: %w", err) + } + + var m map[string]interface{} + if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { + return fmt.Errorf("transforming values file: %w", err) + } + schart.Values = m + + // SaveDir looks for the file values.yaml when saving rather than the values + // key in order to preserve the comments in the YAML. The name placeholder + // needs to be replaced on that file. + for _, f := range schart.Raw { + if f.Name == ValuesfileName { + f.Data = transform(string(f.Data), schart.Name()) + } + } + + return SaveDir(schart, dest) +} + +// Create creates a new chart in a directory. +// +// Inside of dir, this will create a directory based on the name of +// chartfile.Name. It will then write the Chart.yaml into this directory and +// create the (empty) appropriate directories. +// +// The returned string will point to the newly created directory. It will be +// an absolute path, even if the provided base directory was relative. +// +// If dir does not exist, this will return an error. +// If Chart.yaml or any directories cannot be created, this will return an +// error. In such a case, this will attempt to clean up by removing the +// new chart directory. +func Create(name, dir string) (string, error) { + + // Sanity-check the name of a chart so user doesn't create one that causes problems. + if err := validateChartName(name); err != nil { + return "", err + } + + path, err := filepath.Abs(dir) + if err != nil { + return path, err + } + + if fi, err := os.Stat(path); err != nil { + return path, err + } else if !fi.IsDir() { + return path, fmt.Errorf("no such directory %s", path) + } + + cdir := filepath.Join(path, name) + if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { + return cdir, fmt.Errorf("file %s already exists and is not a directory", cdir) + } + + // Note: If adding a new template below (i.e., to `helm create`) which is disabled by default (similar to hpa and + // ingress below); or making an existing template disabled by default, add the enabling condition in + // `TestHelmCreateChart_CheckDeprecatedWarnings` in `pkg/lint/lint_test.go` to make it run through deprecation checks + // with latest Kubernetes version. + files := []struct { + path string + content []byte + }{ + { + // Chart.yaml + path: filepath.Join(cdir, ChartfileName), + content: fmt.Appendf(nil, defaultChartfile, name), + }, + { + // values.yaml + path: filepath.Join(cdir, ValuesfileName), + content: fmt.Appendf(nil, defaultValues, name), + }, + { + // .helmignore + path: filepath.Join(cdir, IgnorefileName), + content: []byte(defaultIgnore), + }, + { + // ingress.yaml + path: filepath.Join(cdir, IngressFileName), + content: transform(defaultIngress, name), + }, + { + // httproute.yaml + path: filepath.Join(cdir, HTTPRouteFileName), + content: transform(defaultHTTPRoute, name), + }, + { + // deployment.yaml + path: filepath.Join(cdir, DeploymentName), + content: transform(defaultDeployment, name), + }, + { + // service.yaml + path: filepath.Join(cdir, ServiceName), + content: transform(defaultService, name), + }, + { + // serviceaccount.yaml + path: filepath.Join(cdir, ServiceAccountName), + content: transform(defaultServiceAccount, name), + }, + { + // hpa.yaml + path: filepath.Join(cdir, HorizontalPodAutoscalerName), + content: transform(defaultHorizontalPodAutoscaler, name), + }, + { + // NOTES.txt + path: filepath.Join(cdir, NotesName), + content: transform(defaultNotes, name), + }, + { + // _helpers.tpl + path: filepath.Join(cdir, HelpersName), + content: transform(defaultHelpers, name), + }, + { + // test-connection.yaml + path: filepath.Join(cdir, TestConnectionName), + content: transform(defaultTestConnection, name), + }, + } + + for _, file := range files { + if _, err := os.Stat(file.path); err == nil { + // There is no handle to a preferred output stream here. + fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path) + } + if err := writeFile(file.path, file.content); err != nil { + return cdir, err + } + } + // Need to add the ChartsDir explicitly as it does not contain any file OOTB + if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil { + return cdir, err + } + return cdir, nil +} + +// transform performs a string replacement of the specified source for +// a given key with the replacement string +func transform(src, replacement string) []byte { + return []byte(strings.ReplaceAll(src, "", replacement)) +} + +func writeFile(name string, content []byte) error { + if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { + return err + } + return os.WriteFile(name, content, 0644) +} + +func validateChartName(name string) error { + if name == "" || len(name) > maxChartNameLength { + return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) + } + if !chartName.MatchString(name) { + return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) + } + return nil +} diff --git a/pkg/helm/pkg/chart/v2/util/create_test.go b/pkg/helm/pkg/chart/v2/util/create_test.go new file mode 100644 index 00000000..34bf6951 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/create_test.go @@ -0,0 +1,173 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "bytes" + "os" + "path/filepath" + "testing" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" +) + +func TestCreate(t *testing.T) { + tdir := t.TempDir() + + c, err := Create("foo", tdir) + if err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + mychart, err := loader.LoadDir(context.Background(), c) + if err != nil { + t.Fatalf("Failed to load newly created chart %q: %s", c, err) + } + + if mychart.Name() != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) + } + + for _, f := range []string{ + ChartfileName, + DeploymentName, + HelpersName, + IgnorefileName, + NotesName, + ServiceAccountName, + ServiceName, + TemplatesDir, + TemplatesTestsDir, + TestConnectionName, + ValuesfileName, + } { + if _, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } + } +} + +func TestCreateFrom(t *testing.T) { + tdir := t.TempDir() + + cf := &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "foo", + Version: "0.1.0", + } + srcdir := "./testdata/frobnitz/charts/mariner" + + if err := CreateFrom(cf, tdir, srcdir); err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + c := filepath.Join(tdir, cf.Name) + mychart, err := loader.LoadDir(context.Background(), c) + if err != nil { + t.Fatalf("Failed to load newly created chart %q: %s", c, err) + } + + if mychart.Name() != "foo" { + t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) + } + + for _, f := range []string{ + ChartfileName, + ValuesfileName, + filepath.Join(TemplatesDir, "placeholder.tpl"), + } { + if _, err := os.Stat(filepath.Join(dir, f)); err != nil { + t.Errorf("Expected %s file: %s", f, err) + } + + // Check each file to make sure has been replaced + b, err := os.ReadFile(filepath.Join(dir, f)) + if err != nil { + t.Errorf("Unable to read file %s: %s", f, err) + } + if bytes.Contains(b, []byte("")) { + t.Errorf("File %s contains ", f) + } + } +} + +// TestCreate_Overwrite is a regression test for making sure that files are overwritten. +func TestCreate_Overwrite(t *testing.T) { + tdir := t.TempDir() + + var errlog bytes.Buffer + + if _, err := Create("foo", tdir); err != nil { + t.Fatal(err) + } + + dir := filepath.Join(tdir, "foo") + + tplname := filepath.Join(dir, "templates/hpa.yaml") + writeFile(tplname, []byte("FOO")) + + // Now re-run the create + Stderr = &errlog + if _, err := Create("foo", tdir); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(tplname) + if err != nil { + t.Fatal(err) + } + + if string(data) == "FOO" { + t.Fatal("File that should have been modified was not.") + } + + if errlog.Len() == 0 { + t.Errorf("Expected warnings about overwriting files.") + } +} + +func TestValidateChartName(t *testing.T) { + for name, shouldPass := range map[string]bool{ + "": false, + "abcdefghijklmnopqrstuvwxyz-_.": true, + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true, + "$hello": false, + "Hellô": false, + "he%%o": false, + "he\nllo": false, + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "abcdefghijklmnopqrstuvwxyz-_." + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false, + } { + if err := validateChartName(name); (err != nil) == shouldPass { + t.Errorf("test for %q failed", name) + } + } +} diff --git a/pkg/helm/pkg/chart/v2/util/dependencies.go b/pkg/helm/pkg/chart/v2/util/dependencies.go new file mode 100644 index 00000000..6eb8881e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/dependencies.go @@ -0,0 +1,767 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "log/slog" + "reflect" + "strings" + + "github.com/werf/nelm/pkg/helm/intern/copystructure" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +// ProcessDependencies checks through this chart's dependencies, processing accordingly. +func ProcessDependencies(c *chart.Chart, v *map[string]interface{}) error { + if v == nil { + return fmt.Errorf("process dependencies: values map pointer is nil") + } + + if err := processDependencyExportExtraValues(c, v, true); err != nil { + return fmt.Errorf("process dependency export extra values: %w", err) + } + + if err := processDependencyEnabled(c, *v, ""); err != nil { + return fmt.Errorf("process dependency enabled: %w", err) + } + + return processDependencyImportExportValues(c, true) +} + +// processDependencyConditions disables charts based on condition path value in values +func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, cpath string) { + if reqs == nil { + return + } + for _, r := range reqs { + for c := range strings.SplitSeq(strings.TrimSpace(r.Condition), ",") { + if len(c) > 0 { + // retrieve value + vv, err := cvals.PathValue(cpath + c) + if err == nil { + // if not bool, warn + if bv, ok := vv.(bool); ok { + r.Enabled = bv + break + } + slog.Warn("returned non-bool value", "path", c, "chart", r.Name) + } else if _, ok := err.(common.ErrNoValue); !ok { + // this is a real error + slog.Warn("the method PathValue returned error", slog.Any("error", err)) + } + } + } + } +} + +// processDependencyTags disables charts based on tags in values +func processDependencyTags(reqs []*chart.Dependency, cvals common.Values) { + if reqs == nil { + return + } + vt, err := cvals.Table("tags") + if err != nil { + return + } + for _, r := range reqs { + var hasTrue, hasFalse bool + for _, k := range r.Tags { + if b, ok := vt[k]; ok { + // if not bool, warn + if bv, ok := b.(bool); ok { + if bv { + hasTrue = true + } else { + hasFalse = true + } + } else { + slog.Warn("returned non-bool value", "tag", k, "chart", r.Name) + } + } + } + if !hasTrue && hasFalse { + r.Enabled = false + } else if hasTrue || !hasTrue && !hasFalse { + r.Enabled = true + } + } +} + +// getAliasDependency finds the chart for an alias dependency and copies parts that will be modified +func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Chart { + for _, c := range charts { + if c == nil { + continue + } + if c.Name() != dep.Name { + continue + } + if !IsCompatibleRange(dep.Version, c.Metadata.Version) { + continue + } + + out := *c + out.Metadata = copyMetadata(c.Metadata) + + // empty dependencies and shallow copy all dependencies, otherwise parent info may be corrupted if + // there is more than one dependency aliasing this chart + out.SetDependencies() + for _, dependency := range c.Dependencies() { + cpy := *dependency + out.AddDependency(&cpy) + } + + if dep.Alias != "" { + out.Metadata.Name = dep.Alias + } + return &out + } + return nil +} + +func copyMetadata(metadata *chart.Metadata) *chart.Metadata { + md := *metadata + + if md.Dependencies != nil { + dependencies := make([]*chart.Dependency, len(md.Dependencies)) + for i := range md.Dependencies { + dependency := *md.Dependencies[i] + dependencies[i] = &dependency + } + md.Dependencies = dependencies + } + return &md +} + +// processDependencyEnabled removes disabled charts from dependencies +func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error { + if c.Metadata.Dependencies == nil { + return nil + } + + var chartDependencies []*chart.Chart + // If any dependency is not a part of Chart.yaml + // then this should be added to chartDependencies. + // However, if the dependency is already specified in Chart.yaml + // we should not add it, as it would be processed from Chart.yaml anyway. + +Loop: + for _, existing := range c.Dependencies() { + for _, req := range c.Metadata.Dependencies { + if existing.Name() == req.Name && IsCompatibleRange(req.Version, existing.Metadata.Version) { + continue Loop + } + } + chartDependencies = append(chartDependencies, existing) + } + + for _, req := range c.Metadata.Dependencies { + if req == nil { + continue + } + if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil { + chartDependencies = append(chartDependencies, chartDependency) + } + if req.Alias != "" { + req.Name = req.Alias + } + } + c.SetDependencies(chartDependencies...) + + // set all to true + for _, lr := range c.Metadata.Dependencies { + lr.Enabled = true + } + cvals, err := util.CoalesceValues(c, v) + if err != nil { + return err + } + // flag dependencies as enabled/disabled + processDependencyTags(c.Metadata.Dependencies, cvals) + processDependencyConditions(c.Metadata.Dependencies, cvals, path) + // make a map of charts to remove + rm := map[string]struct{}{} + for _, r := range c.Metadata.Dependencies { + if !r.Enabled { + // remove disabled chart + rm[r.Name] = struct{}{} + } + } + // don't keep disabled charts in new slice + cd := []*chart.Chart{} + copy(cd, c.Dependencies()[:0]) + for _, n := range c.Dependencies() { + if _, ok := rm[n.Metadata.Name]; !ok { + cd = append(cd, n) + } + } + // don't keep disabled charts in metadata + cdMetadata := []*chart.Dependency{} + copy(cdMetadata, c.Metadata.Dependencies[:0]) + for _, n := range c.Metadata.Dependencies { + if _, ok := rm[n.Name]; !ok { + cdMetadata = append(cdMetadata, n) + } + } + + // recursively call self to process sub dependencies + for _, t := range cd { + subpath := path + t.Metadata.Name + "." + if err := processDependencyEnabled(t, cvals, subpath); err != nil { + return err + } + } + // set the correct dependencies in metadata + c.Metadata.Dependencies = nil + c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...) + c.SetDependencies(cd...) + + return nil +} + +// pathToMap creates a nested map given a YAML path in dot notation. +func pathToMap(path string, data map[string]interface{}) map[string]interface{} { + if path == "." { + return data + } + return set(parsePath(path), data) +} + +func parsePath(key string) []string { return strings.Split(key, ".") } + +func set(path []string, data map[string]interface{}) map[string]interface{} { + if len(path) == 0 { + return nil + } + cur := data + for i := len(path) - 1; i >= 0; i-- { + cur = map[string]interface{}{path[i]: cur} + } + return cur +} + +// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. +func processImportValues(c *chart.Chart, merge bool) error { + if c.Metadata.Dependencies == nil { + return nil + } + // combine chart values and empty config to get Values + var cvals common.Values + var err error + if merge { + cvals, err = util.MergeValues(c, nil) + } else { + cvals, err = util.CoalesceValues(c, nil) + } + if err != nil { + return err + } + b := make(map[string]interface{}) + // import values from each dependency if specified in import-values + for _, r := range c.Metadata.Dependencies { + var outiv []interface{} + for _, riv := range r.ImportValues { + switch iv := riv.(type) { + case map[string]interface{}: + child := fmt.Sprintf("%v", iv["child"]) + parent := fmt.Sprintf("%v", iv["parent"]) + + outiv = append(outiv, map[string]string{ + "child": child, + "parent": parent, + }) + + // get child table + vv, err := cvals.Table(r.Name + "." + child) + if err != nil { + slog.Warn( + "ImportValues missing table from chart", + slog.String("chart", r.Name), + slog.Any("error", err), + ) + continue + } + // create value map from child to be merged into parent + if merge { + b = util.MergeTables(b, pathToMap(parent, vv.AsMap())) + } else { + b = util.CoalesceTables(b, pathToMap(parent, vv.AsMap())) + } + case string: + child := "exports." + iv + outiv = append(outiv, map[string]string{ + "child": child, + "parent": ".", + }) + vm, err := cvals.Table(r.Name + "." + child) + if err != nil { + slog.Warn("ImportValues missing table", slog.Any("error", err)) + continue + } + if merge { + b = util.MergeTables(b, vm.AsMap()) + } else { + b = util.CoalesceTables(b, vm.AsMap()) + } + } + } + r.ImportValues = outiv + } + + // Imported values from a child to a parent chart have a lower priority than + // the parents values. This enables parent charts to import a large section + // from a child and then override select parts. This is why b is merged into + // cvals in the code below and not the other way around. + if merge { + // deep copying the cvals as there are cases where pointers can end + // up in the cvals when they are copied onto b in ways that break things. + cvals = deepCopyMap(cvals) + c.Values = util.MergeTables(cvals, b) + } else { + // Trimming the nil values from cvals is needed for backwards compatibility. + // Previously, the b value had been populated with cvals along with some + // overrides. This caused the coalescing functionality to remove the + // nil/null values. This trimming is for backwards compat. + cvals = trimNilValues(cvals) + c.Values = util.CoalesceTables(cvals, b) + } + + return nil +} + +func deepCopyMap(vals map[string]interface{}) map[string]interface{} { + valsCopy, err := copystructure.Copy(vals) + if err != nil { + return vals + } + return valsCopy.(map[string]interface{}) +} + +func trimNilValues(vals map[string]interface{}) map[string]interface{} { + valsCopy, err := copystructure.Copy(vals) + if err != nil { + return vals + } + valsCopyMap := valsCopy.(map[string]interface{}) + for key, val := range valsCopyMap { + if val == nil { + // Iterate over the values and remove nil keys + delete(valsCopyMap, key) + } else if istable(val) { + // Recursively call into ourselves to remove keys from inner tables + valsCopyMap[key] = trimNilValues(val.(map[string]interface{})) + } + } + + return valsCopyMap +} + +// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. +func istable(v interface{}) bool { + _, ok := v.(map[string]interface{}) + return ok +} + +// processDependencyImportValues imports specified chart values from child to parent. +func processDependencyImportValues(c *chart.Chart, merge bool) error { + for _, d := range c.Dependencies() { + // recurse + if err := processDependencyImportValues(d, merge); err != nil { + return err + } + } + return processImportValues(c, merge) +} + +func processDependencyImportExportValues(c *chart.Chart, merge bool) error { + if err := processDependencyExportValues(c, merge); err != nil { + return fmt.Errorf("process dependency export values: %w", err) + } + + if err := processDependencyImportValues(c, merge); err != nil { + return fmt.Errorf("process dependency import values: %w", err) + } + + return nil +} + +func processDependencyExportValues(c *chart.Chart, merge bool) error { + if err := processExportValues(c, merge); err != nil { + return fmt.Errorf("process export values: %w", err) + } + + for _, d := range c.Dependencies() { + if err := processDependencyExportValues(d, merge); err != nil { + return fmt.Errorf("process dependency export values recursively: %w", err) + } + } + + return nil +} + +func processDependencyExportExtraValues(c *chart.Chart, extraVals *map[string]interface{}, merge bool) error { + if err := processExportExtraValues(c, extraVals, merge); err != nil { + return fmt.Errorf("process export extra values: %w", err) + } + + for _, d := range c.Dependencies() { + if err := processDependencyExportExtraValues(d, extraVals, merge); err != nil { + return fmt.Errorf("process dependency export extra values recursively: %w", err) + } + } + + return nil +} + +func processExportValues(c *chart.Chart, merge bool) error { + if c.Parent() == nil || c.Parent().Metadata.Dependencies == nil { + return nil + } + + r := dependencyForChart(c) + if r == nil { + return nil + } + + pvals, err := valuesForChart(c.Parent(), merge) + if err != nil { + return fmt.Errorf("get parent values: %w", err) + } + + cvals, err := valuesForChart(c, merge) + if err != nil { + return fmt.Errorf("get chart values: %w", err) + } + + exportedValues, err := getExportedValues(c.Parent().Name(), r, pvals, merge) + if err != nil { + return fmt.Errorf("get exported values: %w", err) + } + + if merge { + c.Values = util.MergeTables(deepCopyMap(exportedValues), deepCopyMap(cvals)) + } else { + c.Values = util.CoalesceTables(deepCopyMap(exportedValues), deepCopyMap(cvals)) + } + + if err := syncChartOverridesToParentsValues(c, deepCopyMap(exportedValues), merge); err != nil { + return fmt.Errorf("sync chart overrides to parent values: %w", err) + } + + return nil +} + +func processExportExtraValues(c *chart.Chart, extraVals *map[string]interface{}, merge bool) error { + if extraVals == nil || c.Parent() == nil || c.Parent().Metadata.Dependencies == nil { + return nil + } + + r := dependencyForChart(c) + if r == nil { + return nil + } + + for _, exportValue := range dependencyExportValues(r) { + parent, child, err := parseExportValues(exportValue) + if err != nil { + slog.Warn( + "invalid ExportValues definition", + slog.String("chart", c.Parent().Name()), + slog.String("dependency", r.Name), + slog.Any("error", err), + ) + continue + } + + headlessParentChartPath := stripFirstPathPart(c.Parent().ChartPath()) + exportParentTablePath := parent + if headlessParentChartPath != "" { + exportParentTablePath = joinPath(headlessParentChartPath, parent) + } + + extraParentVals, err := common.Values(*extraVals).Table(exportParentTablePath) + if err != nil { + var errNoTable common.ErrNoTable + if errors.As(err, &errNoTable) { + continue + } + return fmt.Errorf("read extra values table %q: %w", exportParentTablePath, err) + } + + extraChildValsPath := stripFirstPathPart(c.ChartPath()) + if child != "" { + extraChildValsPath = joinPath(extraChildValsPath, child) + } + + var errNoTable common.ErrNoTable + var errNoValue common.ErrNoValue + _, errTable := common.Values(*extraVals).Table(extraChildValsPath) + _, errValue := common.Values(*extraVals).PathValue(extraChildValsPath) + if !(errors.As(errTable, &errNoTable) && errors.As(errValue, &errNoValue)) { + continue + } + + extraChildVals := pathToMap(extraChildValsPath, deepCopyMap(extraParentVals.AsMap())) + if merge { + *extraVals = util.MergeTables(extraChildVals, *extraVals) + } else { + *extraVals = util.CoalesceTables(extraChildVals, *extraVals) + } + } + + return nil +} + +func getExportedValues(parentName string, r *chart.Dependency, pvals common.Values, merge bool) (map[string]interface{}, error) { + b := make(map[string]interface{}) + var normalizedExportValues []interface{} + + for _, rev := range dependencyExportValues(r) { + parent, child, err := parseExportValues(rev) + if err != nil { + slog.Warn( + "invalid ExportValues definition", + slog.String("chart", parentName), + slog.String("dependency", r.Name), + slog.Any("error", err), + ) + continue + } + + normalizedExportValues = append(normalizedExportValues, map[string]string{ + "parent": parent, + "child": child, + }) + + childValMap, ok := valueMapFromExport(parentName, r.Name, pvals, parent, child) + if !ok { + continue + } + + if merge { + b = util.MergeTables(deepCopyMap(childValMap), b) + } else { + b = util.CoalesceTables(deepCopyMap(childValMap), b) + } + } + + setDependencyExportValues(r, normalizedExportValues) + + return b, nil +} + +func valueMapFromExport(parentChartName, dependencyName string, pvals common.Values, parent, child string) (map[string]interface{}, bool) { + vm, err := pvals.Table(parent) + if err == nil { + if child == "" { + return vm.AsMap(), true + } + return pathToMap(child, vm.AsMap()), true + } + + value, valueErr := pvals.PathValue(parent) + if valueErr != nil { + slog.Warn( + "ExportValues parent path not found", + slog.String("chart", parentChartName), + slog.String("dependency", dependencyName), + slog.String("path", parent), + slog.Any("error", err), + ) + return nil, false + } + + childSlice := parsePath(child) + if len(childSlice) == 1 && childSlice[0] == "" { + slog.Warn( + "ExportValues cannot map primitive to root", + slog.String("chart", parentChartName), + slog.String("dependency", dependencyName), + ) + return nil, false + } + + childPath := joinPath(childSlice[:len(childSlice)-1]...) + childMap := map[string]interface{}{childSlice[len(childSlice)-1]: value} + if childPath == "" { + return childMap, true + } + + return pathToMap(childPath, childMap), true +} + +func parseExportValues(rev interface{}) (string, string, error) { + var parent string + var child string + + switch ev := rev.(type) { + case map[string]interface{}: + parentValue, ok := ev["parent"].(string) + if !ok { + return "", "", fmt.Errorf("parent must be a string") + } + + childValue, ok := ev["child"].(string) + if !ok { + return "", "", fmt.Errorf("child must be a string") + } + + parent = strings.TrimSpace(parentValue) + child = strings.TrimSpace(childValue) + if parent == "" || parent == "." { + return "", "", fmt.Errorf("parent %q is not allowed", parentValue) + } + if child == "." { + child = "" + } + case string: + s := strings.TrimSpace(ev) + switch s { + case "", ".": + parent = "exports" + default: + parent = "exports." + s + } + child = "" + default: + return "", "", fmt.Errorf("invalid format of ExportValues") + } + + return parent, child, nil +} + +func syncChartOverridesToParentsValues(c *chart.Chart, overrides map[string]interface{}, merge bool) error { + if c.Parent() == nil { + return nil + } + + pvals, err := valuesForChart(c.Parent(), merge) + if err != nil { + return fmt.Errorf("get parent values: %w", err) + } + + parentOverrides := pathToMap(c.Name(), deepCopyMap(overrides)) + if merge { + c.Parent().Values = util.MergeTables(deepCopyMap(parentOverrides), deepCopyMap(pvals)) + } else { + c.Parent().Values = util.CoalesceTables(deepCopyMap(parentOverrides), deepCopyMap(pvals)) + } + + if err := syncChartOverridesToParentsValues(c.Parent(), parentOverrides, merge); err != nil { + return fmt.Errorf("sync chart overrides recursively: %w", err) + } + + return nil +} + +func valuesForChart(c *chart.Chart, merge bool) (common.Values, error) { + if merge { + vals, err := util.MergeValues(c, nil) + if err != nil { + return nil, err + } + return vals, nil + } + + vals, err := util.CoalesceValues(c, nil) + if err != nil { + return nil, err + } + + return vals, nil +} + +func dependencyForChart(c *chart.Chart) *chart.Dependency { + if c.Parent() == nil || c.Parent().Metadata == nil { + return nil + } + + for _, r := range c.Parent().Metadata.Dependencies { + if r != nil && r.Name == c.Name() { + return r + } + } + + return nil +} + +func dependencyExportValues(r *chart.Dependency) []interface{} { + if r == nil { + return nil + } + + rv := reflect.ValueOf(r) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return nil + } + + elem := rv.Elem() + if !elem.IsValid() { + return nil + } + + field := elem.FieldByName("ExportValues") + if !field.IsValid() || field.Kind() != reflect.Slice { + return nil + } + + result := make([]interface{}, 0, field.Len()) + for i := range field.Len() { + result = append(result, field.Index(i).Interface()) + } + + return result +} + +func setDependencyExportValues(r *chart.Dependency, values []interface{}) { + if r == nil { + return + } + + rv := reflect.ValueOf(r) + if rv.Kind() != reflect.Ptr || rv.IsNil() { + return + } + + elem := rv.Elem() + if !elem.IsValid() { + return + } + + field := elem.FieldByName("ExportValues") + if !field.IsValid() || !field.CanSet() || field.Kind() != reflect.Slice { + return + } + + field.Set(reflect.ValueOf(values)) +} + +func stripFirstPathPart(path string) string { + pathParts := parsePath(path) + if len(pathParts) <= 1 { + return "" + } + + return joinPath(pathParts[1:]...) +} + +func joinPath(path ...string) string { + return strings.Join(path, ".") +} diff --git a/pkg/helm/pkg/chart/v2/util/dependencies_test.go b/pkg/helm/pkg/chart/v2/util/dependencies_test.go new file mode 100644 index 00000000..dfd9b8af --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/dependencies_test.go @@ -0,0 +1,571 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package util + +import ( + "context" + "os" + "path/filepath" + "sort" + "strconv" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" +) + +func loadChart(t *testing.T, path string) *chart.Chart { + t.Helper() + c, err := loader.Load(context.Background(), path) + if err != nil { + t.Fatalf("failed to load testdata: %s", err) + } + return c +} + +func TestLoadDependency(t *testing.T) { + tests := []*chart.Dependency{ + {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, + {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, + } + + check := func(deps []*chart.Dependency) { + if len(deps) != 2 { + t.Errorf("expected 2 dependencies, got %d", len(deps)) + } + for i, tt := range tests { + if deps[i].Name != tt.Name { + t.Errorf("expected dependency named %q, got %q", tt.Name, deps[i].Name) + } + if deps[i].Version != tt.Version { + t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version) + } + if deps[i].Repository != tt.Repository { + t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository) + } + } + } + c := loadChart(t, "testdata/frobnitz") + check(c.Metadata.Dependencies) + check(c.Lock.Dependencies) +} + +func TestDependencyEnabled(t *testing.T) { + type M = map[string]interface{} + tests := []struct { + name string + v M + e []string // expected charts including duplicates in alphanumeric order + }{{ + "tags with no effect", + M{"tags": M{"nothinguseful": false}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, + }, { + "tags disabling a group", + M{"tags": M{"front-end": false}}, + []string{"parentchart"}, + }, { + "tags disabling a group and enabling a different group", + M{"tags": M{"front-end": false, "back-end": true}}, + []string{"parentchart", "parentchart.subchart2", "parentchart.subchart2.subchartb", "parentchart.subchart2.subchartc"}, + }, { + "tags disabling only children, children still enabled since tag front-end=true in values.yaml", + M{"tags": M{"subcharta": false, "subchartb": false}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, + }, { + "tags disabling all parents/children with additional tag re-enabling a parent", + M{"tags": M{"front-end": false, "subchart1": true, "back-end": false}}, + []string{"parentchart", "parentchart.subchart1"}, + }, { + "conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml", + M{"subchart1": M{"enabled": true}, "subchart2": M{"enabled": true}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2"}, + }, { + "conditions disabling the parent charts, effectively disabling children", + M{"subchart1": M{"enabled": false}, "subchart2": M{"enabled": false}}, + []string{"parentchart"}, + }, { + "conditions a child using the second condition path of child's condition", + M{"subchart1": M{"subcharta": M{"enabled": false}}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subchartb"}, + }, { + "tags enabling a parent/child group with condition disabling one child", + M{"subchart2": M{"subchartc": M{"enabled": false}}, "tags": M{"back-end": true}}, + []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2", "parentchart.subchart2.subchartb"}, + }, { + "tags will not enable a child if parent is explicitly disabled with condition", + M{"subchart1": M{"enabled": false}, "tags": M{"front-end": true}}, + []string{"parentchart"}, + }, { + "subcharts with alias also respect conditions", + M{"subchart1": M{"enabled": false}, "subchart2alias": M{"enabled": true, "subchartb": M{"enabled": true}}}, + []string{"parentchart", "parentchart.subchart2alias", "parentchart.subchart2alias.subchartb"}, + }} + + for _, tc := range tests { + c := loadChart(t, "testdata/subpop") + t.Run(tc.name, func(t *testing.T) { + if err := processDependencyEnabled(c, tc.v, ""); err != nil { + t.Fatalf("error processing enabled dependencies %v", err) + } + + names := extractChartNames(c) + if len(names) != len(tc.e) { + t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e)) + } + for i := range names { + if names[i] != tc.e[i] { + t.Fatalf("slice values do not match got %v, expected %v", names, tc.e) + } + } + }) + } +} + +// extractChartNames recursively searches chart dependencies returning all charts found +func extractChartNames(c *chart.Chart) []string { + var out []string + var fn func(c *chart.Chart) + fn = func(c *chart.Chart) { + out = append(out, c.ChartPath()) + for _, d := range c.Dependencies() { + fn(d) + } + } + fn(c) + sort.Strings(out) + return out +} + +func TestProcessDependencyImportValues(t *testing.T) { + c := loadChart(t, "testdata/subpop") + + e := make(map[string]string) + + e["imported-chart1.SC1bool"] = "true" + e["imported-chart1.SC1float"] = "3.14" + e["imported-chart1.SC1int"] = "100" + e["imported-chart1.SC1string"] = "dollywood" + e["imported-chart1.SC1extra1"] = "11" + e["imported-chart1.SPextra1"] = "helm rocks" + e["imported-chart1.SC1extra1"] = "11" + + e["imported-chartA.SCAbool"] = "false" + e["imported-chartA.SCAfloat"] = "3.1" + e["imported-chartA.SCAint"] = "55" + e["imported-chartA.SCAstring"] = "jabba" + e["imported-chartA.SPextra3"] = "1.337" + e["imported-chartA.SC1extra2"] = "1.337" + e["imported-chartA.SCAnested1.SCAnested2"] = "true" + + e["imported-chartA-B.SCAbool"] = "false" + e["imported-chartA-B.SCAfloat"] = "3.1" + e["imported-chartA-B.SCAint"] = "55" + e["imported-chartA-B.SCAstring"] = "jabba" + + e["imported-chartA-B.SCBbool"] = "true" + e["imported-chartA-B.SCBfloat"] = "7.77" + e["imported-chartA-B.SCBint"] = "33" + e["imported-chartA-B.SCBstring"] = "boba" + e["imported-chartA-B.SPextra5"] = "k8s" + e["imported-chartA-B.SC1extra5"] = "tiller" + + // These values are imported from the child chart to the parent. Parent + // values take precedence over imported values. This enables importing a + // large section from a child chart and overriding a selection from it. + e["overridden-chart1.SC1bool"] = "false" + e["overridden-chart1.SC1float"] = "3.141592" + e["overridden-chart1.SC1int"] = "99" + e["overridden-chart1.SC1string"] = "pollywog" + e["overridden-chart1.SPextra2"] = "42" + + e["overridden-chartA.SCAbool"] = "true" + e["overridden-chartA.SCAfloat"] = "41.3" + e["overridden-chartA.SCAint"] = "808" + e["overridden-chartA.SCAstring"] = "jabberwocky" + e["overridden-chartA.SPextra4"] = "true" + + // These values are imported from the child chart to the parent. Parent + // values take precedence over imported values. This enables importing a + // large section from a child chart and overriding a selection from it. + e["overridden-chartA-B.SCAbool"] = "true" + e["overridden-chartA-B.SCAfloat"] = "41.3" + e["overridden-chartA-B.SCAint"] = "808" + e["overridden-chartA-B.SCAstring"] = "jabberwocky" + e["overridden-chartA-B.SCBbool"] = "false" + e["overridden-chartA-B.SCBfloat"] = "1.99" + e["overridden-chartA-B.SCBint"] = "77" + e["overridden-chartA-B.SCBstring"] = "jango" + e["overridden-chartA-B.SPextra6"] = "111" + e["overridden-chartA-B.SCAextra1"] = "23" + e["overridden-chartA-B.SCBextra1"] = "13" + e["overridden-chartA-B.SC1extra6"] = "77" + + // `exports` style + e["SCBexported1B"] = "1965" + e["SC1extra7"] = "true" + e["SCBexported2A"] = "blaster" + e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" + + if err := processDependencyImportValues(c, false); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc := common.Values(c.Values) + for kk, vv := range e { + pv, err := cc.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + + switch pv := pv.(type) { + case float64: + if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { + t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk) + } + case bool: + if b := strconv.FormatBool(pv); b != vv { + t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk) + } + default: + if pv != vv { + t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk) + } + } + } + + // Since this was processed with coalescing there should be no null values. + // Here we verify that. + _, err := cc.PathValue("ensurenull") + if err == nil { + t.Error("expect nil value not found but found it") + } + switch xerr := err.(type) { + case common.ErrNoValue: + // We found what we expected + default: + t.Errorf("expected an ErrNoValue but got %q instead", xerr) + } + + c = loadChart(t, "testdata/subpop") + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc = common.Values(c.Values) + val, err := cc.PathValue("ensurenull") + if err != nil { + t.Error("expect value but ensurenull was not found") + } + if val != nil { + t.Errorf("expect nil value but got %q instead", val) + } +} + +func TestProcessDependencyImportValuesFromSharedDependencyToAliases(t *testing.T) { + c := loadChart(t, "testdata/chart-with-import-from-aliased-dependencies") + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + e := make(map[string]string) + + e["foo-defaults.defaultValue"] = "42" + e["bar-defaults.defaultValue"] = "42" + + e["foo.defaults.defaultValue"] = "42" + e["bar.defaults.defaultValue"] = "42" + + e["foo.grandchild.defaults.defaultValue"] = "42" + e["bar.grandchild.defaults.defaultValue"] = "42" + + cValues := common.Values(c.Values) + for kk, vv := range e { + pv, err := cValues.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + if pv != vv { + t.Errorf("failed to match imported value %v with expected %v", pv, vv) + } + } +} + +func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { + c := loadChart(t, "testdata/three-level-dependent-chart/umbrella") + + e := make(map[string]string) + + // The order of precedence should be: + // 1. User specified values (e.g CLI) + // 2. Parent chart values + // 3. Imported values + // 4. Sub-chart values + // The 4 app charts here deal with things differently: + // - app1 has a port value set in the umbrella chart. It does not import any + // values so the value from the umbrella chart should be used. + // - app2 has a value in the app chart and imports from the library. The + // app chart value should take precedence. + // - app3 has no value in the app chart and imports the value from the library + // chart. The library chart value should be used. + // - app4 has a value in the app chart and does not import the value from the + // library chart. The app charts value should be used. + e["app1.service.port"] = "3456" + e["app2.service.port"] = "8080" + e["app3.service.port"] = "9090" + e["app4.service.port"] = "1234" + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + cc := common.Values(c.Values) + for kk, vv := range e { + pv, err := cc.PathValue(kk) + if err != nil { + t.Fatalf("retrieving import values table %v %v", kk, err) + } + + switch pv := pv.(type) { + case float64: + if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { + t.Errorf("failed to match imported float value %v with expected %v", s, vv) + } + default: + if pv != vv { + t.Errorf("failed to match imported string value %q with expected %q", pv, vv) + } + } + } +} + +func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) { + c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") + nameOverride := "parent-chart-prod" + + if err := processDependencyImportValues(c, true); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 1 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } + + prodDependencyValues := c.Dependencies()[0].Values + if prodDependencyValues["nameOverride"] != nameOverride { + t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"]) + } +} + +func TestGetAliasDependency(t *testing.T) { + c := loadChart(t, "testdata/frobnitz") + req := c.Metadata.Dependencies + + if len(req) == 0 { + t.Fatalf("there are no dependencies to test") + } + + // Success case + aliasChart := getAliasDependency(c.Dependencies(), req[0]) + if aliasChart == nil { + t.Fatalf("failed to get dependency chart for alias %s", req[0].Name) + } + if req[0].Alias != "" { + if aliasChart.Name() != req[0].Alias { + t.Fatalf("dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name()) + } + } else if aliasChart.Name() != req[0].Name { + t.Fatalf("dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name()) + } + + if req[0].Version != "" { + if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { + t.Fatalf("dependency chart version is not in the compatible range") + } + } + + // Failure case + req[0].Name = "something-else" + if aliasChart := getAliasDependency(c.Dependencies(), req[0]); aliasChart != nil { + t.Fatalf("expected no chart but got %s", aliasChart.Name()) + } + + req[0].Version = "something else which is not in the compatible range" + if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { + t.Fatalf("dependency chart version which is not in the compatible range should cause a failure other than a success ") + } +} + +func TestDependentChartAliases(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-alias") + req := c.Metadata.Dependencies + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 3 { + t.Fatal("expected alias dependencies to be added") + } + + if len(c.Dependencies()) != len(c.Metadata.Dependencies) { + t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) + } + + aliasChart := getAliasDependency(c.Dependencies(), req[2]) + + if aliasChart == nil { + t.Fatalf("failed to get dependency chart for alias %s", req[2].Name) + } + if aliasChart.Parent() != c { + t.Fatalf("dependency chart has wrong parent, expected %s but got %s", c.Name(), aliasChart.Parent().Name()) + } + if req[2].Alias != "" { + if aliasChart.Name() != req[2].Alias { + t.Fatalf("dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name()) + } + } else if aliasChart.Name() != req[2].Name { + t.Fatalf("dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name()) + } + + req[2].Name = "dummy-name" + if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil { + t.Fatalf("expected no chart but got %s", aliasChart.Name()) + } + +} + +func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-no-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } +} + +func TestDependentChartWithSubChartsHelmignore(t *testing.T) { + // FIXME what does this test? + loadChart(t, "testdata/dependent-chart-helmignore") +} + +func TestDependentChartsWithSubChartsSymlink(t *testing.T) { + joonix := filepath.Join("testdata", "joonix") + if err := os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")); err != nil { + t.Fatal(err) + } + defer os.RemoveAll(filepath.Join(joonix, "charts", "frobnitz")) + c := loadChart(t, joonix) + + if c.Name() != "joonix" { + t.Fatalf("unexpected chart name: %s", c.Name()) + } + if n := len(c.Dependencies()); n != 1 { + t.Fatalf("expected 1 dependency for this chart, but got %d", n) + } +} + +func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-with-all-in-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Dependencies()) != len(c.Metadata.Dependencies) { + t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) + } +} + +func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) { + c := loadChart(t, "testdata/dependent-chart-with-mixed-requirements-yaml") + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } +} + +func validateDependencyTree(t *testing.T, c *chart.Chart) { + t.Helper() + for _, dependency := range c.Dependencies() { + if dependency.Parent() != c { + if dependency.Parent() != c { + t.Fatalf("dependency chart %s has wrong parent, expected %s but got %s", dependency.Name(), c.Name(), dependency.Parent().Name()) + } + } + // recurse entire tree + validateDependencyTree(t, dependency) + } +} + +func TestChartWithDependencyAliasedTwiceAndDoublyReferencedSubDependency(t *testing.T) { + c := loadChart(t, "testdata/chart-with-dependency-aliased-twice") + + if len(c.Dependencies()) != 1 { + t.Fatalf("expected one dependency for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatal("expected two dependencies after processing aliases") + } + validateDependencyTree(t, c) +} diff --git a/pkg/helm/pkg/chart/v2/util/deps_migration_ai_test.go b/pkg/helm/pkg/chart/v2/util/deps_migration_ai_test.go new file mode 100644 index 00000000..04f28158 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/deps_migration_ai_test.go @@ -0,0 +1,57 @@ +//go:build ai_tests + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func TestAI_ProcessDependenciesCallable(t *testing.T) { + parent := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "parent", + Version: "1.0.0", + APIVersion: chart.APIVersionV2, + }, + Values: map[string]interface{}{ + "key": "value", + }, + } + + child := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "child", + Version: "0.1.0", + APIVersion: chart.APIVersionV2, + }, + Values: map[string]interface{}{}, + } + parent.SetDependencies(child) + + vals := map[string]interface{}{ + "key": "value", + } + + err := ProcessDependencies(parent, &vals) + require.NoError(t, err) + assert.NotNil(t, vals) +} + +func TestAI_ProcessDependenciesRejectsNilVals(t *testing.T) { + parent := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "parent", + Version: "1.0.0", + APIVersion: chart.APIVersionV2, + }, + } + + err := ProcessDependencies(parent, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "nil") +} diff --git a/pkg/helm/pkg/chart/v2/util/doc.go b/pkg/helm/pkg/chart/v2/util/doc.go new file mode 100644 index 00000000..32b709b4 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/doc.go @@ -0,0 +1,45 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +package util contains tools for working with charts. + +Charts are described in the chart package (pkg/chart). +This package provides utilities for serializing and deserializing charts. + +A chart can be represented on the file system in one of two ways: + + - As a directory that contains a Chart.yaml file and other chart things. + - As a tarred gzipped file containing a directory that then contains a + Chart.yaml file. + +This package provides utilities for working with those file formats. + +The preferred way of loading a chart is using 'loader.Load`: + + chart, err := loader.Load(filename) + +This will attempt to discover whether the file at 'filename' is a directory or +a chart archive. It will then load accordingly. + +For accepting raw compressed tar file data from an io.Reader, the +'loader.LoadArchive()' will read in the data, uncompress it, and unpack it +into a Chart. + +When creating charts in memory, use the 'github.com/werf/nelm/pkg/helm/pkg/chart' +package directly. +*/ +package util // import chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" diff --git a/pkg/helm/pkg/chart/v2/util/expand.go b/pkg/helm/pkg/chart/v2/util/expand.go new file mode 100644 index 00000000..3537867a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/expand.go @@ -0,0 +1,94 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "io" + "os" + "path/filepath" + + securejoin "github.com/cyphar/filepath-securejoin" + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/loader/archive" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +// Expand uncompresses and extracts a chart into the specified directory. +func Expand(dir string, r io.Reader) error { + files, err := archive.LoadArchiveFiles(r) + if err != nil { + return err + } + + // Get the name of the chart + var chartName string + for _, file := range files { + if file.Name == "Chart.yaml" { + ch := &chart.Metadata{} + if err := yaml.Unmarshal(file.Data, ch); err != nil { + return fmt.Errorf("cannot load Chart.yaml: %w", err) + } + chartName = ch.Name + } + } + if chartName == "" { + return errors.New("chart name not specified") + } + + // Find the base directory + // The directory needs to be cleaned prior to passing to SecureJoin or the location may end up + // being wrong or returning an error. This was introduced in v0.4.0. + dir = filepath.Clean(dir) + chartdir, err := securejoin.SecureJoin(dir, chartName) + if err != nil { + return err + } + + // Copy all files verbatim. We don't parse these files because parsing can remove + // comments. + for _, file := range files { + outpath, err := securejoin.SecureJoin(chartdir, file.Name) + if err != nil { + return err + } + + // Make sure the necessary subdirs get created. + basedir := filepath.Dir(outpath) + if err := os.MkdirAll(basedir, 0755); err != nil { + return err + } + + if err := os.WriteFile(outpath, file.Data, 0644); err != nil { + return err + } + } + + return nil +} + +// ExpandFile expands the src file into the dest directory. +func ExpandFile(dest, src string) error { + h, err := os.Open(src) + if err != nil { + return err + } + defer h.Close() + return Expand(dest, h) +} diff --git a/pkg/helm/pkg/chart/v2/util/expand_test.go b/pkg/helm/pkg/chart/v2/util/expand_test.go new file mode 100644 index 00000000..280995f7 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/expand_test.go @@ -0,0 +1,124 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "os" + "path/filepath" + "testing" +) + +func TestExpand(t *testing.T) { + dest := t.TempDir() + + reader, err := os.Open("testdata/frobnitz-1.2.3.tgz") + if err != nil { + t.Fatal(err) + } + + if err := Expand(dest, reader); err != nil { + t.Fatal(err) + } + + expectedChartPath := filepath.Join(dest, "frobnitz") + fi, err := os.Stat(expectedChartPath) + if err != nil { + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("expected a chart directory at %s", expectedChartPath) + } + + dir, err := os.Open(expectedChartPath) + if err != nil { + t.Fatal(err) + } + + fis, err := dir.Readdir(0) + if err != nil { + t.Fatal(err) + } + + expectLen := 11 + if len(fis) != expectLen { + t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) + } + + for _, fi := range fis { + expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) + if err != nil { + t.Fatal(err) + } + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat always returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } + } + } +} + +func TestExpandFile(t *testing.T) { + dest := t.TempDir() + + if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil { + t.Fatal(err) + } + + expectedChartPath := filepath.Join(dest, "frobnitz") + fi, err := os.Stat(expectedChartPath) + if err != nil { + t.Fatal(err) + } + if !fi.IsDir() { + t.Fatalf("expected a chart directory at %s", expectedChartPath) + } + + dir, err := os.Open(expectedChartPath) + if err != nil { + t.Fatal(err) + } + + fis, err := dir.Readdir(0) + if err != nil { + t.Fatal(err) + } + + expectLen := 11 + if len(fis) != expectLen { + t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) + } + + for _, fi := range fis { + expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) + if err != nil { + t.Fatal(err) + } + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat always returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } + } + } +} diff --git a/pkg/helm/pkg/chart/v2/util/save.go b/pkg/helm/pkg/chart/v2/util/save.go new file mode 100644 index 00000000..1cf4b5f7 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/save.go @@ -0,0 +1,287 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "archive/tar" + "compress/gzip" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "time" + + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") + +func SaveDir(c *chart.Chart, dest string) error { + return SaveIntoDir(c, filepath.Join(dest, c.Name())) +} + +func SaveIntoDir(c *chart.Chart, dest string) error { + err := validateName(c.Name()) + if err != nil { + return err + } + outdir := dest + if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() { + return fmt.Errorf("file %s already exists and is not a directory", outdir) + } + if err := os.MkdirAll(outdir, 0755); err != nil { + return err + } + + // Save the chart file. + if err := SaveChartfile(filepath.Join(outdir, ChartfileName), c.Metadata); err != nil { + return err + } + + if c.Metadata.APIVersion == chart.APIVersionV2 { + if c.Lock != nil { + ldata, err := yaml.Marshal(c.Lock) + if err != nil { + return err + } + filename := filepath.Join(outdir, "Chart.lock") + if err := writeFile(filename, ldata); err != nil { + return fmt.Errorf("write %q: %w", filename, err) + } + } + } + + // Save values.yaml + for _, f := range c.Raw { + if f.Name == ValuesfileName { + vf := filepath.Join(outdir, ValuesfileName) + if err := writeFile(vf, f.Data); err != nil { + return err + } + } + } + + // Save values.schema.json if it exists + if c.Schema != nil { + filename := filepath.Join(outdir, SchemafileName) + if err := writeFile(filename, c.Schema); err != nil { + return err + } + } + + for _, o := range [][]*common.File{c.Templates, c.Files, c.RuntimeFiles} { + for _, f := range o { + n := filepath.Join(outdir, f.Name) + if err := writeFile(n, f.Data); err != nil { + return err + } + } + } + + // Save dependencies + base := filepath.Join(outdir, ChartsDir) + for _, dep := range c.Dependencies() { + // Here, we write each dependency as a tar file. + if _, err := Save(dep, base); err != nil { + return fmt.Errorf("saving %s: %w", dep.ChartFullPath(), err) + } + } + return nil +} + +// Save creates an archived chart to the given directory. +// +// This takes an existing chart and a destination directory. +// +// If the directory is /foo, and the chart is named bar, with version 1.0.0, this +// will generate /foo/bar-1.0.0.tgz. +// +// This returns the absolute path to the chart archive file. +func Save(c *chart.Chart, outDir string) (string, error) { + if err := c.Validate(); err != nil { + return "", fmt.Errorf("chart validation: %w", err) + } + + filename := fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version) + filename = filepath.Join(outDir, filename) + dir := filepath.Dir(filename) + if stat, err := os.Stat(dir); err != nil { + if errors.Is(err, fs.ErrNotExist) { + if err2 := os.MkdirAll(dir, 0755); err2 != nil { + return "", err2 + } + } else { + return "", fmt.Errorf("stat %s: %w", dir, err) + } + } else if !stat.IsDir() { + return "", fmt.Errorf("is not a directory: %s", dir) + } + + f, err := os.Create(filename) + if err != nil { + return "", err + } + + // Wrap in gzip writer + zipper := gzip.NewWriter(f) + zipper.Extra = headerBytes + zipper.Comment = "Helm" + + // Wrap in tar writer + twriter := tar.NewWriter(zipper) + rollback := false + defer func() { + twriter.Close() + zipper.Close() + f.Close() + if rollback { + os.Remove(filename) + } + }() + + if err := writeTarContents(twriter, c, ""); err != nil { + rollback = true + return filename, err + } + return filename, nil +} + +func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { + err := validateName(c.Name()) + if err != nil { + return err + } + base := filepath.Join(prefix, c.Name()) + + // Pull out the dependencies of a v1 Chart, since there's no way + // to tell the serializer to skip a field for just this use case + savedDependencies := c.Metadata.Dependencies + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Metadata.Dependencies = nil + } + // Save Chart.yaml + cdata, err := yaml.Marshal(c.Metadata) + if c.Metadata.APIVersion == chart.APIVersionV1 { + c.Metadata.Dependencies = savedDependencies + } + if err != nil { + return err + } + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil { + return err + } + + // Save Chart.lock + // TODO: remove the APIVersion check when APIVersionV1 is not used anymore + if c.Metadata.APIVersion == chart.APIVersionV2 { + if c.Lock != nil { + ldata, err := yaml.Marshal(c.Lock) + if err != nil { + return err + } + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil { + return err + } + } + } + + // Save values.yaml + for _, f := range c.Raw { + if f.Name == ValuesfileName { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil { + return err + } + } + } + + // Save values.schema.json if it exists + if c.Schema != nil { + if !json.Valid(c.Schema) { + return errors.New("invalid JSON in " + SchemafileName) + } + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil { + return err + } + } + + // Save templates + for _, f := range c.Templates { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { + return err + } + } + + // Save files + for _, f := range c.Files { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { + return err + } + } + + for _, f := range c.RuntimeFiles { + n := filepath.Join(base, f.Name) + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { + return err + } + } + + // Save dependencies + for _, dep := range c.Dependencies() { + if err := writeTarContents(out, dep, filepath.Join(base, ChartsDir)); err != nil { + return err + } + } + return nil +} + +// writeToTar writes a single file to a tar archive. +func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error { + // TODO: Do we need to create dummy parent directory names if none exist? + h := &tar.Header{ + Name: filepath.ToSlash(name), + Mode: 0644, + Size: int64(len(body)), + ModTime: modTime, + } + if h.ModTime.IsZero() { + h.ModTime = time.Now() + } + if err := out.WriteHeader(h); err != nil { + return err + } + _, err := out.Write(body) + return err +} + +// If the name has directory name has characters which would change the location +// they need to be removed. +func validateName(name string) error { + nname := filepath.Base(name) + + if nname != name { + return common.ErrInvalidChartName{Name: name} + } + + return nil +} diff --git a/pkg/helm/pkg/chartutil/save_extended.go b/pkg/helm/pkg/chart/v2/util/save_extended.go similarity index 84% rename from pkg/helm/pkg/chartutil/save_extended.go rename to pkg/helm/pkg/chart/v2/util/save_extended.go index 37ff357d..75416eba 100644 --- a/pkg/helm/pkg/chartutil/save_extended.go +++ b/pkg/helm/pkg/chart/v2/util/save_extended.go @@ -1,10 +1,10 @@ -package chartutil +package util import ( "archive/tar" "compress/gzip" - "github.com/werf/nelm/pkg/helm/pkg/chart" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" ) type SaveIntoTarOptions struct { diff --git a/pkg/helm/pkg/chart/v2/util/save_test.go b/pkg/helm/pkg/chart/v2/util/save_test.go new file mode 100644 index 00000000..a1335daa --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/save_test.go @@ -0,0 +1,362 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "context" + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha256" + "errors" + "fmt" + "io" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" +) + +func TestSave(t *testing.T) { + tmp := t.TempDir() + + for _, dest := range []string{tmp, filepath.Join(tmp, "newdir")} { + t.Run("outDir="+dest, func(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + } + chartWithInvalidJSON := withSchema(*c, []byte("{")) + + where, err := Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + if !strings.HasPrefix(where, dest) { + t.Fatalf("Expected %q to start with %q", where, dest) + } + if !strings.HasSuffix(where, ".tgz") { + t.Fatalf("Expected %q to end with .tgz", where) + } + + c2, err := loader.LoadFile(context.Background(), where) + if err != nil { + t.Fatal(err) + } + if c2.Name() != c.Name() { + t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) + } + if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { + t.Fatal("Files data did not match") + } + if c2.Lock != nil { + t.Fatal("Expected v1 chart archive not to contain Chart.lock file") + } + + if !bytes.Equal(c.Schema, c2.Schema) { + indentation := 4 + formattedExpected := Indent(indentation, string(c.Schema)) + formattedActual := Indent(indentation, string(c2.Schema)) + t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual) + } + if _, err := Save(&chartWithInvalidJSON, dest); err == nil { + t.Fatalf("Invalid JSON was not caught while saving chart") + } + + c.Metadata.APIVersion = chart.APIVersionV2 + where, err = Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + c2, err = loader.LoadFile(context.Background(), where) + if err != nil { + t.Fatal(err) + } + if c2.Lock == nil { + t.Fatal("Expected v2 chart archive to contain a Chart.lock file") + } + if c2.Lock.Digest != c.Lock.Digest { + t.Fatal("Chart.lock data did not match") + } + }) + } + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "../ahab", + Version: "1.2.3", + }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, + }, + } + _, err := Save(c, tmp) + if err == nil { + t.Fatal("Expected error saving chart with invalid name") + } +} + +// Creates a copy with a different schema; does not modify anything. +func withSchema(chart chart.Chart, schema []byte) chart.Chart { + chart.Schema = schema + return chart +} + +func Indent(n int, text string) string { + startOfLine := regexp.MustCompile(`(?m)^`) + indentation := strings.Repeat(" ", n) + return startOfLine.ReplaceAllLiteralString(text, indentation) +} + +func TestSavePreservesTimestamps(t *testing.T) { + // Test executes so quickly that if we don't subtract a second, the + // check will fail because `initialCreateTime` will be identical to the + // written timestamp for the files. + initialCreateTime := time.Now().Add(-1 * time.Second) + + tmp := t.TempDir() + + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: initialCreateTime, + Values: map[string]interface{}{ + "imageName": "testimage", + "imageId": 42, + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: initialCreateTime, + } + + where, err := Save(c, tmp) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + + allHeaders, err := retrieveAllHeadersFromTar(where) + if err != nil { + t.Fatalf("Failed to parse tar: %v", err) + } + + roundedTime := initialCreateTime.Round(time.Second) + for _, header := range allHeaders { + if !header.ModTime.Equal(roundedTime) { + t.Fatalf("File timestamp not preserved: %v", header.ModTime) + } + } +} + +// We could refactor `load.go` to use this `retrieveAllHeadersFromTar` function +// as well, so we are not duplicating components of the code which iterate +// through the tar. +func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { + raw, err := os.Open(path) + if err != nil { + return nil, err + } + defer raw.Close() + + unzipped, err := gzip.NewReader(raw) + if err != nil { + return nil, err + } + defer unzipped.Close() + + tr := tar.NewReader(unzipped) + headers := []*tar.Header{} + for { + hd, err := tr.Next() + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, err + } + + headers = append(headers, hd) + } + + return headers, nil +} + +func TestSaveDir(t *testing.T) { + tmp := t.TempDir() + + modTime := time.Now() + c := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Templates: []*common.File{ + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")}, + }, + } + + if err := SaveDir(c, tmp); err != nil { + t.Fatalf("Failed to save: %s", err) + } + + c2, err := loader.LoadDir(context.Background(), tmp + "/ahab") + if err != nil { + t.Fatal(err) + } + + if c2.Name() != c.Name() { + t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) + } + + if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name { + t.Fatal("Templates data did not match") + } + + if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name { + t.Fatal("Files data did not match") + } + + tmp2 := t.TempDir() + c.Metadata.Name = "../ahab" + pth := filepath.Join(tmp2, "tmpcharts") + if err := os.MkdirAll(filepath.Join(pth), 0755); err != nil { + t.Fatal(err) + } + + if err := SaveDir(c, pth); err.Error() != "\"../ahab\" is not a valid chart name" { + t.Fatalf("Did not get expected error for chart named %q", c.Name()) + } +} + +func TestRepeatableSave(t *testing.T) { + tmp := t.TempDir() + defer os.RemoveAll(tmp) + modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC) + tests := []struct { + name string + chart *chart.Chart + want string + }{ + { + name: "Package 1 file", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV2, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + Generated: modTime, + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "fea2662522317b65c2788ff9e5fc446a9264830038dac618d4449493d99b3257", + }, + { + name: "Package 2 files", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV2, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + Generated: modTime, + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + {Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "7ae92b2f274bb51ea3f1969e4187d78cc52b5f6f663b44b8fb3b40bcb8ee46f3", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create package + dest := path.Join(tmp, "newdir") + where, err := Save(test.chart, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + // get shasum for package + result, err := sha256Sum(where) + if err != nil { + t.Fatalf("Failed to check shasum: %s", err) + } + // assert that the package SHA is what we wanted. + if result != test.want { + t.Errorf("FormatName() result = %v, want %v", result, test.want) + } + }) + } +} + +func sha256Sum(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml new file mode 100644 index 00000000..d778f8fe --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +appVersion: 1.0.0 +name: chart-with-dependency-aliased-twice +type: application +version: 1.0.0 + +dependencies: + - name: child + alias: foo + version: 1.0.0 + - name: child + alias: bar + version: 1.0.0 + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml new file mode 100644 index 00000000..220fda66 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: 1.0.0 +name: child +type: application +version: 1.0.0 + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml new file mode 100644 index 00000000..50e620a8 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: 1.0.0 +name: grandchild +type: application +version: 1.0.0 + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml new file mode 100644 index 00000000..1830492e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }}-{{ .Values.from }} +data: + {{- toYaml .Values | nindent 2 }} + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml new file mode 100644 index 00000000..b5d55af7 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }} +data: + {{- toYaml .Values | nindent 2 }} + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/values.yaml new file mode 100644 index 00000000..695521a4 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-dependency-aliased-twice/values.yaml @@ -0,0 +1,7 @@ +foo: + grandchild: + from: foo +bar: + grandchild: + from: bar + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml new file mode 100644 index 00000000..c408f0ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +appVersion: 1.0.0 +name: chart-with-dependency-aliased-twice +type: application +version: 1.0.0 + +dependencies: + - name: child + alias: foo + version: 1.0.0 + import-values: + - parent: foo-defaults + child: defaults + - name: child + alias: bar + version: 1.0.0 + import-values: + - parent: bar-defaults + child: defaults + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml new file mode 100644 index 00000000..ecdaf04d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml @@ -0,0 +1,12 @@ +apiVersion: v2 +appVersion: 1.0.0 +name: child +type: application +version: 1.0.0 + +dependencies: + - name: grandchild + version: 1.0.0 + import-values: + - parent: defaults + child: defaults diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml new file mode 100644 index 00000000..50e620a8 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +appVersion: 1.0.0 +name: grandchild +type: application +version: 1.0.0 + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml new file mode 100644 index 00000000..f51c594f --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml @@ -0,0 +1,2 @@ +defaults: + defaultValue: "42" \ No newline at end of file diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml new file mode 100644 index 00000000..3140f53d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }} +data: + {{ .Values.defaults | toYaml }} + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml new file mode 100644 index 00000000..a2b62c95 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Chart.Name }} +data: + {{ toYaml .Values.defaults | indent 2 }} + diff --git a/pkg/helm/pkg/chartutil/testdata/chartfiletest.yaml b/pkg/helm/pkg/chart/v2/util/testdata/chartfiletest.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/chartfiletest.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/chartfiletest.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/.helmignore b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/Chart.lock b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/Chart.lock new file mode 100644 index 00000000..6fcc2ed9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/INSTALL.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/LICENSE b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/_ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..3190136b Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/docs/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/icon.svg b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/ignore/me.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/ignore/me.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/templates/template.tpl b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-alias/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/.helmignore b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/.helmignore new file mode 100644 index 00000000..8a71bc82 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/.helmignore @@ -0,0 +1,2 @@ +ignore/ +.* diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/.ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/.ignore_me new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/_ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..5bbae10a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/templates/template.tpl b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-helmignore/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/.helmignore b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/LICENSE b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..5bbae10a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..3190136b Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/icon.svg b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-no-requirements-yaml/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..5bbae10a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..3190136b Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..5bbae10a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz new file mode 100644 index 00000000..3190136b Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz-1.2.3.tgz similarity index 100% rename from pkg/helm/pkg/repo/testdata/repository/frobnitz-1.2.3.tgz rename to pkg/helm/pkg/chart/v2/util/testdata/frobnitz-1.2.3.tgz diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/.helmignore b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/.helmignore new file mode 100644 index 00000000..9973a57b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/Chart.lock b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/Chart.lock new file mode 100644 index 00000000..6fcc2ed9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/Chart.lock @@ -0,0 +1,8 @@ +dependencies: + - name: alpine + version: "0.1.0" + repository: https://example.com/charts + - name: mariner + version: "4.3.2" + repository: https://example.com/charts +digest: invalid diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/frobnitz/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/INSTALL.txt b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/INSTALL.txt new file mode 100644 index 00000000..2010438c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/LICENSE b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/LICENSE new file mode 100644 index 00000000..6121943b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/README.md b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/README.md new file mode 100644 index 00000000..8cf4cc3d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/README.md @@ -0,0 +1,11 @@ +# Frobnitz + +This is an example chart. + +## Usage + +This is an example. It has no usage. + +## Development + +For developer info, see the top-level repository. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/_ignore_me b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/_ignore_me new file mode 100644 index 00000000..2cecca68 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/README.md b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/README.md new file mode 100644 index 00000000..b30b949d --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/README.md @@ -0,0 +1,9 @@ +This example was generated using the command `helm create alpine`. + +The `templates/` directory contains a very simple pod resource with a +couple of parameters. + +The `values.toml` file contains the default values for the +`alpine-pod.yaml` template. + +You can install this example using `helm install ./alpine`. diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 00000000..42c39c26 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml @@ -0,0 +1,4 @@ +# Default values for mast1. +# This is a YAML-formatted file. +# Declare name/value pairs to be passed into your templates. +# name = "value" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 00000000..61cb6205 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..5bbae10a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/values.yaml new file mode 100644 index 00000000..6c2aab7b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml new file mode 100644 index 00000000..3121cd7c --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml @@ -0,0 +1,4 @@ +albatross: "true" + +global: + author: Coleridge diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl new file mode 100644 index 00000000..29c11843 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl @@ -0,0 +1 @@ +# This is a placeholder. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/values.yaml new file mode 100644 index 00000000..b0ccb008 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/charts/mariner/values.yaml @@ -0,0 +1,7 @@ +# Default values for . +# This is a YAML-formatted file. https://github.com/toml-lang/toml +# Declare name/value pairs to be passed into your templates. +# name: "value" + +: + test: true diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/docs/README.md b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/docs/README.md new file mode 100644 index 00000000..d40747ca --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/icon.svg b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/icon.svg new file mode 100644 index 00000000..89213060 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/ignore/me.txt b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/ignore/me.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/templates/template.tpl b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/templates/template.tpl new file mode 100644 index 00000000..c651ee6a --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/values.yaml new file mode 100644 index 00000000..61f50125 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/pkg/helm/pkg/chart/v2/util/testdata/frobnitz_backslash-1.2.3.tgz b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz_backslash-1.2.3.tgz new file mode 100644 index 00000000..69296595 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/frobnitz_backslash-1.2.3.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/genfrob.sh b/pkg/helm/pkg/chart/v2/util/testdata/genfrob.sh new file mode 100755 index 00000000..35fdd59f --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/genfrob.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +# Pack the albatross chart into the mariner chart. +echo "Packing albatross into mariner" +tar -zcvf mariner/charts/albatross-0.1.0.tgz albatross + +echo "Packing mariner into frobnitz" +tar -zcvf frobnitz/charts/mariner-4.3.2.tgz mariner +tar -zcvf frobnitz_backslash/charts/mariner-4.3.2.tgz mariner + +# Pack the frobnitz chart. +echo "Packing frobnitz" +tar --exclude=ignore/* -zcvf frobnitz-1.2.3.tgz frobnitz +tar --exclude=ignore/* -zcvf frobnitz_backslash-1.2.3.tgz frobnitz_backslash diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock new file mode 100644 index 00000000..b2f17fb3 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: dev + repository: file://envs/dev + version: v0.1.0 +- name: prod + repository: file://envs/prod + version: v0.1.0 +digest: sha256:9403fc24f6cf9d6055820126cf7633b4bd1fed3c77e4880c674059f536346182 +generated: "2020-02-03T10:38:51.180474+01:00" diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz new file mode 100644 index 00000000..d28e1621 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz differ diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz new file mode 100644 index 00000000..a0c5aa84 Binary files /dev/null and b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz differ diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml new file mode 100644 index 00000000..38f03484 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml @@ -0,0 +1,9 @@ +# Dev values parent-chart +nameOverride: parent-chart-dev +exports: + data: + resources: + autoscaler: + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 diff --git a/pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml new file mode 100644 index 00000000..10cc756b --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml @@ -0,0 +1,9 @@ +# Prod values parent-chart +nameOverride: parent-chart-prod +exports: + data: + resources: + autoscaler: + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 90 diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml new file mode 100644 index 00000000..976e5a8f --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml @@ -0,0 +1,16 @@ +################################################################################################### +# parent-chart horizontal pod autoscaler +################################################################################################### +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-autoscaler + namespace: {{ .Release.Namespace }} +spec: + scaleTargetRef: + apiVersion: apps/v1beta1 + kind: Deployment + name: {{ .Release.Name }} + minReplicas: {{ required "A valid .Values.resources.autoscaler.minReplicas entry required!" .Values.resources.autoscaler.minReplicas }} + maxReplicas: {{ required "A valid .Values.resources.autoscaler.maxReplicas entry required!" .Values.resources.autoscaler.maxReplicas }} + targetCPUUtilizationPercentage: {{ required "A valid .Values.resources.autoscaler.targetCPUUtilizationPercentage!" .Values.resources.autoscaler.targetCPUUtilizationPercentage }} \ No newline at end of file diff --git a/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml new file mode 100644 index 00000000..b812f0a3 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml @@ -0,0 +1,10 @@ +# Default values for parent-chart. +nameOverride: parent-chart +tags: + dev: false + prod: true +resources: + autoscaler: + minReplicas: 0 + maxReplicas: 0 + targetCPUUtilizationPercentage: 99 \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/joonix/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/joonix/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/joonix/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/joonix/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/joonix/charts/.gitkeep b/pkg/helm/pkg/chart/v2/util/testdata/joonix/charts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/Chart.yaml new file mode 100644 index 00000000..27118672 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/Chart.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +description: A Helm chart for Kubernetes +name: parentchart +version: 0.1.0 +dependencies: + - name: subchart1 + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart1.enabled + tags: + - front-end + - subchart1 + import-values: + - child: SC1data + parent: imported-chart1 + - child: SC1data + parent: overridden-chart1 + - child: imported-chartA + parent: imported-chartA + - child: imported-chartA-B + parent: imported-chartA-B + - child: overridden-chartA-B + parent: overridden-chartA-B + - child: SCBexported1A + parent: . + - SCBexported2 + - SC1exported1 + + - name: subchart2 + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart2.enabled + tags: + - back-end + - subchart2 + + - name: subchart2 + alias: subchart2alias + repository: http://localhost:10191 + version: 0.1.0 + condition: subchart2alias.enabled diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/README.md b/pkg/helm/pkg/chart/v2/util/testdata/subpop/README.md new file mode 100644 index 00000000..e43fbfe9 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/README.md @@ -0,0 +1,18 @@ +## Subpop + +This chart is for testing the processing of enabled/disabled charts +via conditions and tags. + +Currently there are three levels: + +```` +parent +-1 tags: front-end, subchart1 +--A tags: front-end, subchartA +--B tags: front-end, subchartB +-2 tags: back-end, subchart2 +--B tags: back-end, subchartB +--C tags: back-end, subchartC +```` + +Tags and conditions are currently in requirements.yaml files. \ No newline at end of file diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/Chart.yaml new file mode 100644 index 00000000..9d8c03ee --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/Chart.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +description: A Helm chart for Kubernetes +name: subchart1 +version: 0.1.0 +dependencies: + - name: subcharta + repository: http://localhost:10191 + version: 0.1.0 + condition: subcharta.enabled + tags: + - front-end + - subcharta + import-values: + - child: SCAdata + parent: imported-chartA + - child: SCAdata + parent: overridden-chartA + - child: SCAdata + parent: imported-chartA-B + + - name: subchartb + repository: http://localhost:10191 + version: 0.1.0 + condition: subchartb.enabled + import-values: + - child: SCBdata + parent: imported-chartB + - child: SCBdata + parent: imported-chartA-B + - child: exports.SCBexported2 + parent: exports.SCBexported2 + - SCBexported1 + + tags: + - front-end + - subchartb diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartA/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartA/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/templates/service.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml new file mode 100644 index 00000000..f0381ae6 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml @@ -0,0 +1,17 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# subchartA +service: + name: apache + type: ClusterIP + externalPort: 80 + internalPort: 80 +SCAdata: + SCAbool: false + SCAfloat: 3.1 + SCAint: 55 + SCAstring: "jabba" + SCAnested1: + SCAnested2: true + diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartB/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartB/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/noreqs/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/noreqs/templates/service.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml new file mode 100644 index 00000000..774fdd75 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml @@ -0,0 +1,35 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 + +SCBdata: + SCBbool: true + SCBfloat: 7.77 + SCBint: 33 + SCBstring: "boba" + +exports: + SCBexported1: + SCBexported1A: + SCBexported1B: 1965 + + SCBexported2: + SCBexported2A: "blaster" + +global: + kolla: + nova: + api: + all: + port: 8774 + metadata: + all: + port: 8775 + + + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/crds/crdA.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/crds/crdA.yaml new file mode 100644 index 00000000..fca77fd4 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/crds/crdA.yaml @@ -0,0 +1,13 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: testCRDs +spec: + group: testCRDGroups + names: + kind: TestCRD + listKind: TestCRDList + plural: TestCRDs + shortNames: + - tc + singular: authconfig diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/NOTES.txt b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/NOTES.txt similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/NOTES.txt rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/NOTES.txt diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/service.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/service.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/service.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml new file mode 100644 index 00000000..91b954e5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml @@ -0,0 +1,7 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ .Chart.Name }}-role +rules: +- resources: ["*"] + verbs: ["get","list","watch"] diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/values.yaml new file mode 100644 index 00000000..a974e316 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart1/values.yaml @@ -0,0 +1,55 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# subchart1 +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 + + +SC1data: + SC1bool: true + SC1float: 3.14 + SC1int: 100 + SC1string: "dollywood" + SC1extra1: 11 + +imported-chartA: + SC1extra2: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 3.14 + SCAint: 100 + SCAstring: "jabbathehut" + SC1extra3: true + +imported-chartA-B: + SC1extra5: "tiller" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 3.33 + SCAint: 555 + SCAstring: "wormwood" + SCAextra1: 23 + + SCBbool: true + SCBfloat: 0.25 + SCBint: 98 + SCBstring: "murkwood" + SCBextra1: 13 + + SC1extra6: 77 + +SCBexported1A: + SC1extra7: true + +exports: + SC1exported1: + global: + SC1exported2: + all: + SC1exported3: "SC1expstr" \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml new file mode 100644 index 00000000..fb3dfc44 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: subchart2-{{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: subchart2-{{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml new file mode 100644 index 00000000..5e5b2106 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml @@ -0,0 +1,21 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml new file mode 100644 index 00000000..27501e1e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml new file mode 100644 index 00000000..5e5b2106 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml @@ -0,0 +1,21 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/templates/service.yaml new file mode 100644 index 00000000..27501e1e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/values.yaml new file mode 100644 index 00000000..5e5b2106 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/charts/subchart2/values.yaml @@ -0,0 +1,21 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/noreqs/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/noreqs/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/templates/service.yaml new file mode 100644 index 00000000..27501e1e --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/values.yaml new file mode 100644 index 00000000..4ed3b7ad --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/noreqs/values.yaml @@ -0,0 +1,26 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +replicaCount: 1 +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +service: + name: nginx + type: ClusterIP + externalPort: 80 + internalPort: 80 +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi + + +# switch-like +tags: + front-end: true + back-end: false diff --git a/pkg/helm/pkg/chart/v2/util/testdata/subpop/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/subpop/values.yaml new file mode 100644 index 00000000..ba70ed40 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/subpop/values.yaml @@ -0,0 +1,45 @@ +# parent/values.yaml + +imported-chart1: + SPextra1: "helm rocks" + +overridden-chart1: + SC1bool: false + SC1float: 3.141592 + SC1int: 99 + SC1string: "pollywog" + SPextra2: 42 + + +imported-chartA: + SPextra3: 1.337 + +overridden-chartA: + SCAbool: true + SCAfloat: 41.3 + SCAint: 808 + SCAstring: "jabberwocky" + SPextra4: true + +imported-chartA-B: + SPextra5: "k8s" + +overridden-chartA-B: + SCAbool: true + SCAfloat: 41.3 + SCAint: 808 + SCAstring: "jabberwocky" + SCBbool: false + SCBfloat: 1.99 + SCBint: 77 + SCBstring: "jango" + SPextra6: 111 + +tags: + front-end: true + back-end: false + +subchart2alias: + enabled: false + +ensurenull: null diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/README.md b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/README.md new file mode 100644 index 00000000..536bb979 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/README.md @@ -0,0 +1,16 @@ +# Three Level Dependent Chart + +This chart is for testing the processing of multi-level dependencies. + +Consists of the following charts: + +- Library Chart +- App Chart (Uses Library Chart as dependency, 2x: app1/app2) +- Umbrella Chart (Has all the app charts as dependencies) + +The precedence is as follows: `library < app < umbrella` + +Catches two use-cases: + +- app overwriting library (app2) +- umbrella overwriting app and library (app1) diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml new file mode 100644 index 00000000..3fd398b5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml new file mode 100644 index 00000000..0c08b6cd --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml new file mode 100644 index 00000000..8ed8ddf1 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml new file mode 100644 index 00000000..3728aa93 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 1234 diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml new file mode 100644 index 00000000..3fd398b5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml new file mode 100644 index 00000000..0c08b6cd --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml new file mode 100644 index 00000000..8ed8ddf1 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml new file mode 100644 index 00000000..98bd6d24 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 8080 diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml new file mode 100644 index 00000000..3fd398b5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml new file mode 100644 index 00000000..0c08b6cd --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml new file mode 100644 index 00000000..8ed8ddf1 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml new file mode 100644 index 00000000..b738e2a5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml @@ -0,0 +1,2 @@ +service: + type: ClusterIP diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml rename to pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml new file mode 100644 index 00000000..3fd398b5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml new file mode 100644 index 00000000..0c08b6cd --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml @@ -0,0 +1,5 @@ +exports: + defaults: + service: + type: ClusterIP + port: 9090 diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml new file mode 100644 index 00000000..8ed8ddf1 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml new file mode 100644 index 00000000..3728aa93 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 1234 diff --git a/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/values.yaml b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/values.yaml new file mode 100644 index 00000000..de0bafa5 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/testdata/three-level-dependent-chart/umbrella/values.yaml @@ -0,0 +1,14 @@ +app1: + enabled: true + service: + type: ClusterIP + port: 3456 + +app2: + enabled: true + +app3: + enabled: true + +app4: + enabled: true diff --git a/pkg/helm/pkg/chart/v2/util/validate_name.go b/pkg/helm/pkg/chart/v2/util/validate_name.go new file mode 100644 index 00000000..6595e085 --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/validate_name.go @@ -0,0 +1,111 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "errors" + "fmt" + "regexp" +) + +// validName is a regular expression for resource names. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + +var ( + // errMissingName indicates that a release (name) was not provided. + errMissingName = errors.New("no name provided") + + // errInvalidName indicates that an invalid release name was provided + errInvalidName = fmt.Errorf( + "invalid release name, must match regex %s and the length must not be longer than 53", + validName.String()) + + // errInvalidKubernetesName indicates that the name does not meet the Kubernetes + // restrictions on metadata names. + errInvalidKubernetesName = fmt.Errorf( + "invalid metadata name, must match regex %s and the length must not be longer than 253", + validName.String()) +) + +const ( + // According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names) + // some resource names have a max length of 63 characters while others have a max + // length of 253 characters. As we cannot be sure the resources used in a chart, we + // therefore need to limit it to 63 chars and reserve 10 chars for additional part to name + // of the resource. The reason is that chart maintainers can use release name as part of + // the resource name (and some additional chars). + maxReleaseNameLen = 53 + // maxMetadataNameLen is the maximum length Kubernetes allows for any name. + maxMetadataNameLen = 253 +) + +// ValidateReleaseName performs checks for an entry for a Helm release name +// +// For Helm to allow a name, it must be below a certain character count (53) and also match +// a regular expression. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +func ValidateReleaseName(name string) error { + // This case is preserved for backwards compatibility + if name == "" { + return errMissingName + + } + if len(name) > maxReleaseNameLen || !validName.MatchString(name) { + return errInvalidName + } + return nil +} + +// ValidateMetadataName validates the name field of a Kubernetes metadata object. +// +// Empty strings, strings longer than 253 chars, or strings that don't match the regexp +// will fail. +// +// According to the Kubernetes help text, the regular expression it uses is: +// +// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* +// +// This follows the above regular expression (but requires a full string match, not partial). +// +// The Kubernetes documentation is here, though it is not entirely correct: +// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// Deprecated: remove in Helm 4. Name validation now uses rules defined in +// pkg/lint/rules.validateMetadataNameFunc() +func ValidateMetadataName(name string) error { + if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) { + return errInvalidKubernetesName + } + return nil +} diff --git a/pkg/helm/pkg/chart/v2/util/validate_name_test.go b/pkg/helm/pkg/chart/v2/util/validate_name_test.go new file mode 100644 index 00000000..cfc62a0f --- /dev/null +++ b/pkg/helm/pkg/chart/v2/util/validate_name_test.go @@ -0,0 +1,91 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import "testing" + +// TestValidateReleaseName is a regression test for ValidateName +// +// Kubernetes has strict naming conventions for resource names. This test represents +// those conventions. +// +// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names +// +// NOTE: At the time of this writing, the docs above say that names cannot begin with +// digits. However, `kubectl`'s regular expression explicit allows this, and +// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits. +func TestValidateReleaseName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + "a1111111111111111111111111111111111111111111111111111111111z": false, + } + for input, expectPass := range names { + if err := ValidateReleaseName(input); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} + +func TestValidateMetadataName(t *testing.T) { + names := map[string]bool{ + "": false, + "foo": true, + "foo.bar1234baz.seventyone": true, + "FOO": false, + "123baz": true, + "foo.BAR.baz": false, + "one-two": true, + "-two": false, + "one_two": false, + "a..b": false, + "%^&#$%*@^*@&#^": false, + "example:com": false, + "example%%com": false, + "a1111111111111111111111111111111111111111111111111111111111z": true, + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z" + + "a1111111111111111111111111111111111111111111111111111111111z": false, + } + for input, expectPass := range names { + if err := ValidateMetadataName(input); (err == nil) != expectPass { + st := "fail" + if expectPass { + st = "succeed" + } + t.Errorf("Expected %q to %s", input, st) + } + } +} diff --git a/pkg/helm/pkg/chartutil/capabilities.go b/pkg/helm/pkg/chartutil/capabilities.go deleted file mode 100644 index 6908577d..00000000 --- a/pkg/helm/pkg/chartutil/capabilities.go +++ /dev/null @@ -1,126 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "fmt" - "strconv" - - "github.com/Masterminds/semver/v3" - "k8s.io/client-go/kubernetes/scheme" - - apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" - - helmversion "github.com/werf/nelm/pkg/helm/intern/version" -) - -var ( - // The Kubernetes version can be set by LDFLAGS. In order to do that the value - // must be a string. - k8sVersionMajor = "1" - k8sVersionMinor = "20" - - // DefaultVersionSet is the default version set, which includes only Core V1 ("v1"). - DefaultVersionSet = allKnownVersions() - - // DefaultCapabilities is the default set of capabilities. - DefaultCapabilities = &Capabilities{ - KubeVersion: KubeVersion{ - Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor), - Major: k8sVersionMajor, - Minor: k8sVersionMinor, - }, - APIVersions: DefaultVersionSet, - HelmVersion: helmversion.Get(), - } -) - -// Capabilities describes the capabilities of the Kubernetes cluster. -type Capabilities struct { - // KubeVersion is the Kubernetes version. - KubeVersion KubeVersion - // APIversions are supported Kubernetes API versions. - APIVersions VersionSet - // HelmVersion is the build information for this helm version - HelmVersion helmversion.BuildInfo -} - -func (capabilities *Capabilities) Copy() *Capabilities { - return &Capabilities{ - KubeVersion: capabilities.KubeVersion, - APIVersions: capabilities.APIVersions, - HelmVersion: capabilities.HelmVersion, - } -} - -// KubeVersion is the Kubernetes version. -type KubeVersion struct { - Version string // Kubernetes version - Major string // Kubernetes major version - Minor string // Kubernetes minor version -} - -// String implements fmt.Stringer -func (kv *KubeVersion) String() string { return kv.Version } - -// GitVersion returns the Kubernetes version string. -// -// Deprecated: use KubeVersion.Version. -func (kv *KubeVersion) GitVersion() string { return kv.Version } - -// ParseKubeVersion parses kubernetes version from string -func ParseKubeVersion(version string) (*KubeVersion, error) { - sv, err := semver.NewVersion(version) - if err != nil { - return nil, err - } - return &KubeVersion{ - Version: "v" + sv.String(), - Major: strconv.FormatUint(sv.Major(), 10), - Minor: strconv.FormatUint(sv.Minor(), 10), - }, nil -} - -// VersionSet is a set of Kubernetes API versions. -type VersionSet []string - -// Has returns true if the version string is in the set. -// -// vs.Has("apps/v1") -func (v VersionSet) Has(apiVersion string) bool { - for _, x := range v { - if x == apiVersion { - return true - } - } - return false -} - -func allKnownVersions() VersionSet { - // We should register the built in extension APIs as well so CRDs are - // supported in the default version set. This has caused problems with `helm - // template` in the past, so let's be safe - apiextensionsv1beta1.AddToScheme(scheme.Scheme) - apiextensionsv1.AddToScheme(scheme.Scheme) - - groups := scheme.Scheme.PrioritizedVersionsAllGroups() - vs := make(VersionSet, 0, len(groups)) - for _, gv := range groups { - vs = append(vs, gv.String()) - } - return vs -} diff --git a/pkg/helm/pkg/chartutil/capabilities_test.go b/pkg/helm/pkg/chartutil/capabilities_test.go deleted file mode 100644 index b58d7e0f..00000000 --- a/pkg/helm/pkg/chartutil/capabilities_test.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "testing" -) - -func TestVersionSet(t *testing.T) { - vs := VersionSet{"v1", "apps/v1"} - if d := len(vs); d != 2 { - t.Errorf("Expected 2 versions, got %d", d) - } - - if !vs.Has("apps/v1") { - t.Error("Expected to find apps/v1") - } - - if vs.Has("Spanish/inquisition") { - t.Error("No one expects the Spanish/inquisition") - } -} - -func TestDefaultVersionSet(t *testing.T) { - if !DefaultVersionSet.Has("v1") { - t.Error("Expected core v1 version set") - } -} - -func TestDefaultCapabilities(t *testing.T) { - kv := DefaultCapabilities.KubeVersion - if kv.String() != "v1.20.0" { - t.Errorf("Expected default KubeVersion.String() to be v1.20.0, got %q", kv.String()) - } - if kv.Version != "v1.20.0" { - t.Errorf("Expected default KubeVersion.Version to be v1.20.0, got %q", kv.Version) - } - if kv.GitVersion() != "v1.20.0" { - t.Errorf("Expected default KubeVersion.GitVersion() to be v1.20.0, got %q", kv.Version) - } - if kv.Major != "1" { - t.Errorf("Expected default KubeVersion.Major to be 1, got %q", kv.Major) - } - if kv.Minor != "20" { - t.Errorf("Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) - } -} - -func TestDefaultCapabilitiesHelmVersion(t *testing.T) { - hv := DefaultCapabilities.HelmVersion - - if hv.Version != "v3.14" { - t.Errorf("Expected default HelmVersion to be v3.14, got %q", hv.Version) - } -} - -func TestParseKubeVersion(t *testing.T) { - kv, err := ParseKubeVersion("v1.16.0") - if err != nil { - t.Errorf("Expected v1.16.0 to parse successfully") - } - if kv.Version != "v1.16.0" { - t.Errorf("Expected parsed KubeVersion.Version to be v1.16.0, got %q", kv.String()) - } - if kv.Major != "1" { - t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major) - } - if kv.Minor != "16" { - t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor) - } -} diff --git a/pkg/helm/pkg/chartutil/chartfile.go b/pkg/helm/pkg/chartutil/chartfile.go deleted file mode 100644 index a7f0d54d..00000000 --- a/pkg/helm/pkg/chartutil/chartfile.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "os" - "path/filepath" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -// LoadChartfile loads a Chart.yaml file into a *chart.Metadata. -func LoadChartfile(filename string) (*chart.Metadata, error) { - b, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - y := new(chart.Metadata) - err = yaml.Unmarshal(b, y) - return y, err -} - -// SaveChartfile saves the given metadata as a Chart.yaml file at the given path. -// -// 'filename' should be the complete path and filename ('foo/Chart.yaml') -func SaveChartfile(filename string, cf *chart.Metadata) error { - // Pull out the dependencies of a v1 Chart, since there's no way - // to tell the serializer to skip a field for just this use case - savedDependencies := cf.Dependencies - if cf.APIVersion == chart.APIVersionV1 { - cf.Dependencies = nil - } - out, err := yaml.Marshal(cf) - if cf.APIVersion == chart.APIVersionV1 { - cf.Dependencies = savedDependencies - } - if err != nil { - return err - } - return os.WriteFile(filename, out, 0644) -} - -// IsChartDir validate a chart directory. -// -// Checks for a valid Chart.yaml. -func IsChartDir(dirName string) (bool, error) { - if fi, err := os.Stat(dirName); err != nil { - return false, err - } else if !fi.IsDir() { - return false, errors.Errorf("%q is not a directory", dirName) - } - - chartYaml := filepath.Join(dirName, ChartfileName) - if _, err := os.Stat(chartYaml); os.IsNotExist(err) { - return false, errors.Errorf("no %s exists in directory %q", ChartfileName, dirName) - } - - chartYamlContent, err := os.ReadFile(chartYaml) - if err != nil { - return false, errors.Errorf("cannot read %s in directory %q", ChartfileName, dirName) - } - - chartContent := new(chart.Metadata) - if err := yaml.Unmarshal(chartYamlContent, &chartContent); err != nil { - return false, err - } - if chartContent == nil { - return false, errors.Errorf("chart metadata (%s) missing", ChartfileName) - } - if chartContent.Name == "" { - return false, errors.Errorf("invalid chart (%s): name must not be empty", ChartfileName) - } - - return true, nil -} diff --git a/pkg/helm/pkg/chartutil/chartfile_test.go b/pkg/helm/pkg/chartutil/chartfile_test.go deleted file mode 100644 index 0c66069b..00000000 --- a/pkg/helm/pkg/chartutil/chartfile_test.go +++ /dev/null @@ -1,121 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -const testfile = "testdata/chartfiletest.yaml" - -func TestLoadChartfile(t *testing.T) { - f, err := LoadChartfile(testfile) - if err != nil { - t.Errorf("Failed to open %s: %s", testfile, err) - return - } - verifyChartfile(t, f, "frobnitz") -} - -func verifyChartfile(t *testing.T, f *chart.Metadata, name string) { - - if f == nil { //nolint:staticcheck - t.Fatal("Failed verifyChartfile because f is nil") - } - - if f.APIVersion != chart.APIVersionV1 { //nolint:staticcheck - t.Errorf("Expected API Version %q, got %q", chart.APIVersionV1, f.APIVersion) - } - - if f.Name != name { - t.Errorf("Expected %s, got %s", name, f.Name) - } - - if f.Description != "This is a frobnitz." { - t.Errorf("Unexpected description %q", f.Description) - } - - if f.Version != "1.2.3" { - t.Errorf("Unexpected version %q", f.Version) - } - - if len(f.Maintainers) != 2 { - t.Errorf("Expected 2 maintainers, got %d", len(f.Maintainers)) - } - - if f.Maintainers[0].Name != "The Helm Team" { - t.Errorf("Unexpected maintainer name.") - } - - if f.Maintainers[1].Email != "nobody@example.com" { - t.Errorf("Unexpected maintainer email.") - } - - if len(f.Sources) != 1 { - t.Fatalf("Unexpected number of sources") - } - - if f.Sources[0] != "https://example.com/foo/bar" { - t.Errorf("Expected https://example.com/foo/bar, got %s", f.Sources) - } - - if f.Home != "http://example.com" { - t.Error("Unexpected home.") - } - - if f.Icon != "https://example.com/64x64.png" { - t.Errorf("Unexpected icon: %q", f.Icon) - } - - if len(f.Keywords) != 3 { - t.Error("Unexpected keywords") - } - - if len(f.Annotations) != 2 { - t.Fatalf("Unexpected annotations") - } - - if want, got := "extravalue", f.Annotations["extrakey"]; want != got { - t.Errorf("Want %q, but got %q", want, got) - } - - if want, got := "anothervalue", f.Annotations["anotherkey"]; want != got { - t.Errorf("Want %q, but got %q", want, got) - } - - kk := []string{"frobnitz", "sprocket", "dodad"} - for i, k := range f.Keywords { - if kk[i] != k { - t.Errorf("Expected %q, got %q", kk[i], k) - } - } -} - -func TestIsChartDir(t *testing.T) { - validChartDir, err := IsChartDir("testdata/frobnitz") - if !validChartDir { - t.Errorf("unexpected error while reading chart-directory: (%v)", err) - return - } - validChartDir, err = IsChartDir("testdata") - if validChartDir || err == nil { - t.Errorf("expected error but did not get any") - return - } -} diff --git a/pkg/helm/pkg/chartutil/coalesce.go b/pkg/helm/pkg/chartutil/coalesce.go deleted file mode 100644 index fcfdfecb..00000000 --- a/pkg/helm/pkg/chartutil/coalesce.go +++ /dev/null @@ -1,347 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "context" - "fmt" - "log" - - "github.com/mitchellh/copystructure" - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets" -) - -func concatPrefix(a, b string) string { - if a == "" { - return b - } - return fmt.Sprintf("%s.%s", a, b) -} - -// CoalesceValues coalesces all of the values in a chart (and its subcharts). -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - A chart has access to all of the variables for it, as well as all of -// the values destined for its dependencies. -func CoalesceValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - vals, err := makeValues(chrt, vals) - if err != nil { - return vals, err - } - - valsCopy, err := copyValues(vals) - if err != nil { - return vals, err - } - return coalesce(log.Printf, chrt, valsCopy, "", false) -} - -// MergeValues is used to merge the values in a chart and its subcharts. This -// is different from Coalescing as nil/null values are preserved. -// -// Values are coalesced together using the following rules: -// -// - Values in a higher level chart always override values in a lower-level -// dependency chart -// - Scalar values and arrays are replaced, maps are merged -// - A chart has access to all of the variables for it, as well as all of -// the values destined for its dependencies. -// -// Retaining Nils is useful when processes early in a Helm action or business -// logic need to retain them for when Coalescing will happen again later in the -// business logic. -func MergeValues(chrt *chart.Chart, vals map[string]interface{}) (Values, error) { - vals, err := makeValues(chrt, vals) - if err != nil { - return vals, err - } - - valsCopy, err := copyValues(vals) - if err != nil { - return vals, err - } - return coalesce(log.Printf, chrt, valsCopy, "", true) -} - -func copyValues(vals map[string]interface{}) (Values, error) { - v, err := copystructure.Copy(vals) - if err != nil { - return vals, err - } - - valsCopy := v.(map[string]interface{}) - // if we have an empty map, make sure it is initialized - if valsCopy == nil { - valsCopy = make(map[string]interface{}) - } - - return valsCopy, nil -} - -type printFn func(format string, v ...interface{}) - -// coalesce coalesces the dest values and the chart values, giving priority to the dest values. -// -// This is a helper function for CoalesceValues and MergeValues. -// -// Note, the merge argument specifies whether this is being used by MergeValues -// or CoalesceValues. Coalescing removes null values and their keys in some -// situations while merging keeps the null values. -func coalesce(printf printFn, ch *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - coalesceValues(printf, ch, dest, prefix, merge) - return coalesceDeps(printf, ch, dest, prefix, merge) -} - -// coalesceDeps coalesces the dependencies of the given chart. -func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}, prefix string, merge bool) (map[string]interface{}, error) { - for _, subchart := range chrt.Dependencies() { - if c, ok := dest[subchart.Name()]; !ok { - // If dest doesn't already have the key, create it. - dest[subchart.Name()] = make(map[string]interface{}) - } else if !istable(c) { - return dest, errors.Errorf("type mismatch on %s: %t", subchart.Name(), c) - } - if dv, ok := dest[subchart.Name()]; ok { - dvmap := dv.(map[string]interface{}) - subPrefix := concatPrefix(prefix, chrt.Metadata.Name) - // Get globals out of dest and merge them into dvmap. - coalesceGlobals(printf, dvmap, dest, subPrefix, merge) - // Now coalesce the rest of the values. - var err error - dest[subchart.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge) - if err != nil { - return dest, err - } - } - } - return dest, nil -} - -// coalesceGlobals copies the globals out of src and merges them into dest. -// -// For convenience, returns dest. -func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) { - var dg, sg map[string]interface{} - - if destglob, ok := dest[GlobalKey]; !ok { - dg = make(map[string]interface{}) - } else if dg, ok = destglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because destination %s is not a table.", GlobalKey) - return - } - - if srcglob, ok := src[GlobalKey]; !ok { - sg = make(map[string]interface{}) - } else if sg, ok = srcglob.(map[string]interface{}); !ok { - printf("warning: skipping globals because source %s is not a table.", GlobalKey) - return - } - - // EXPERIMENTAL: In the past, we have disallowed globals to test tables. This - // reverses that decision. It may somehow be possible to introduce a loop - // here, but I haven't found a way. So for the time being, let's allow - // tables in globals. - for key, val := range sg { - if istable(val) { - vv := copyMap(val.(map[string]interface{})) - if destv, ok := dg[key]; !ok { - // Here there is no merge. We're just adding. - dg[key] = vv - } else { - if destvmap, ok := destv.(map[string]interface{}); !ok { - printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key) - } else { - // Basically, we reverse order of coalesce here to merge - // top-down. - subPrefix := concatPrefix(prefix, key) - // In this location coalesceTablesFullKey should always have - // merge set to true. The output of coalesceGlobals is run - // through coalesce where any nils will be removed. - coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true) - dg[key] = vv - } - } - } else if dv, ok := dg[key]; ok && istable(dv) { - // It's not clear if this condition can actually ever trigger. - printf("key %s is table. Skipping", key) - } else { - // TODO: Do we need to do any additional checking on the value? - dg[key] = val - } - } - dest[GlobalKey] = dg -} - -func copyMap(src map[string]interface{}) map[string]interface{} { - m := make(map[string]interface{}, len(src)) - for k, v := range src { - m[k] = v - } - return m -} - -// coalesceValues builds up a values map for a particular chart. -// -// Values in v will override the values in the chart. -func coalesceValues(printf printFn, c *chart.Chart, v map[string]interface{}, prefix string, merge bool) { - subPrefix := concatPrefix(prefix, c.Metadata.Name) - - // Using c.Values directly when coalescing a table can cause problems where - // the original c.Values is altered. Creating a deep copy stops the problem. - // This section is fault-tolerant as there is no ability to return an error. - valuesCopy, err := copystructure.Copy(c.Values) - var vc map[string]interface{} - var ok bool - if err != nil { - // If there is an error something is wrong with copying c.Values it - // means there is a problem in the deep copying package or something - // wrong with c.Values. In this case we will use c.Values and report - // an error. - printf("warning: unable to copy values, err: %s", err) - vc = c.Values - } else { - vc, ok = valuesCopy.(map[string]interface{}) - if !ok { - // c.Values has a map[string]interface{} structure. If the copy of - // it cannot be treated as map[string]interface{} there is something - // strangely wrong. Log it and use c.Values - printf("warning: unable to convert values copy to values type") - vc = c.Values - } - } - - for key, val := range vc { - if value, ok := v[key]; ok { - if value == nil && !merge { - // When the YAML value is null and we are coalescing instead of - // merging, we remove the value's key. - // This allows Helm's various sources of values (value files or --set) to - // remove incompatible keys from any previous chart, file, or set values. - delete(v, key) - } else if dest, ok := value.(map[string]interface{}); ok { - // if v[key] is a table, merge nv's val table into v[key]. - src, ok := val.(map[string]interface{}) - if !ok { - // If the original value is nil, there is nothing to coalesce, so we don't print - // the warning - if val != nil { - printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key) - } - } else { - // Because v has higher precedence than nv, dest values override src - // values. - coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge) - } - } - } else { - // If the key is not in v, copy it from nv. - v[key] = val - } - } -} - -// CoalesceTables merges a source map into a destination map. -// -// dest is considered authoritative. -func CoalesceTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "", false) -} - -func MergeTables(dst, src map[string]interface{}) map[string]interface{} { - return coalesceTablesFullKey(log.Printf, dst, src, "", true) -} - -// coalesceTablesFullKey merges a source map into a destination map. -// -// dest is considered authoritative. -func coalesceTablesFullKey(printf printFn, dst, src map[string]interface{}, prefix string, merge bool) map[string]interface{} { - // When --reuse-values is set but there are no modifications yet, return new values - if src == nil { - return dst - } - if dst == nil { - return src - } - // Because dest has higher precedence than src, dest values override src - // values. - for key, val := range src { - fullkey := concatPrefix(prefix, key) - if dv, ok := dst[key]; ok && !merge && dv == nil { - delete(dst, key) - } else if !ok { - dst[key] = val - } else if istable(val) { - if istable(dv) { - coalesceTablesFullKey(printf, dv.(map[string]interface{}), val.(map[string]interface{}), fullkey, merge) - } else { - printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val) - } - } else if istable(dv) && val != nil { - printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val) - } - } - return dst -} - -func makeValues(chrt *chart.Chart, vals map[string]interface{}) (map[string]interface{}, error) { - var decryptedSecretValues map[string]interface{} - if chrt.SecretsRuntimeData != nil { - decryptedSecretValues = chrt.SecretsRuntimeData.GetDecryptedSecretValues() - } - - var extraValues map[string]interface{} - if chrt.ExtraValues != nil { - extraValues = chrt.ExtraValues - } - - result, err := MergeInternal( - context.Background(), - vals, - extraValues, - decryptedSecretValues, - ) - if err != nil { - return vals, err - } - - return result, nil -} - -func MergeInternal(ctx context.Context, inputVals, serviceVals map[string]interface{}, decryptedSecretValues map[string]interface{}) (map[string]interface{}, error) { - vals := make(map[string]interface{}) - - CoalesceTables(vals, serviceVals) // NOTE: service values will not be saved into the marshalled release - - if decryptedSecretValues != nil { - CoalesceTables(vals, decryptedSecretValues) - } - - CoalesceTables(vals, inputVals) - - return vals, nil -} - -func init() { - secrets.CoalesceTablesFunc = CoalesceTables -} diff --git a/pkg/helm/pkg/chartutil/compatible.go b/pkg/helm/pkg/chartutil/compatible.go deleted file mode 100644 index f4656c91..00000000 --- a/pkg/helm/pkg/chartutil/compatible.go +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import "github.com/Masterminds/semver/v3" - -// IsCompatibleRange compares a version to a constraint. -// It returns true if the version matches the constraint, and false in all other cases. -func IsCompatibleRange(constraint, ver string) bool { - sv, err := semver.NewVersion(ver) - if err != nil { - return false - } - - c, err := semver.NewConstraint(constraint) - if err != nil { - return false - } - return c.Check(sv) -} diff --git a/pkg/helm/pkg/chartutil/compatible_test.go b/pkg/helm/pkg/chartutil/compatible_test.go deleted file mode 100644 index df7be616..00000000 --- a/pkg/helm/pkg/chartutil/compatible_test.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package version represents the current version of the project. -package chartutil - -import "testing" - -func TestIsCompatibleRange(t *testing.T) { - tests := []struct { - constraint string - ver string - expected bool - }{ - {"v2.0.0-alpha.4", "v2.0.0-alpha.4", true}, - {"v2.0.0-alpha.3", "v2.0.0-alpha.4", false}, - {"v2.0.0", "v2.0.0-alpha.4", false}, - {"v2.0.0-alpha.4", "v2.0.0", false}, - {"~v2.0.0", "v2.0.1", true}, - {"v2", "v2.0.0", true}, - {">2.0.0", "v2.1.1", true}, - {"v2.1.*", "v2.1.1", true}, - } - - for _, tt := range tests { - if IsCompatibleRange(tt.constraint, tt.ver) != tt.expected { - t.Errorf("expected constraint %s to be %v for %s", tt.constraint, tt.expected, tt.ver) - } - } -} diff --git a/pkg/helm/pkg/chartutil/create.go b/pkg/helm/pkg/chartutil/create.go deleted file mode 100644 index 95c39a58..00000000 --- a/pkg/helm/pkg/chartutil/create.go +++ /dev/null @@ -1,724 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "fmt" - "io" - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -// chartName is a regular expression for testing the supplied name of a chart. -// This regular expression is probably stricter than it needs to be. We can relax it -// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be -// problematic. -var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$") - -const ( - // ChartfileName is the default Chart file name. - ChartfileName = "Chart.yaml" - // ValuesfileName is the default values file name. - ValuesfileName = "values.yaml" - // SchemafileName is the default values schema file name. - SchemafileName = "values.schema.json" - // TemplatesDir is the relative directory name for templates. - TemplatesDir = "templates" - // ChartsDir is the relative directory name for charts dependencies. - ChartsDir = "charts" - // TemplatesTestsDir is the relative directory name for tests. - TemplatesTestsDir = TemplatesDir + sep + "tests" - // IgnorefileName is the name of the Helm ignore file. - IgnorefileName = ".helmignore" - // IngressFileName is the name of the example ingress file. - IngressFileName = TemplatesDir + sep + "ingress.yaml" - // DeploymentName is the name of the example deployment file. - DeploymentName = TemplatesDir + sep + "deployment.yaml" - // ServiceName is the name of the example service file. - ServiceName = TemplatesDir + sep + "service.yaml" - // ServiceAccountName is the name of the example serviceaccount file. - ServiceAccountName = TemplatesDir + sep + "serviceaccount.yaml" - // HorizontalPodAutoscalerName is the name of the example hpa file. - HorizontalPodAutoscalerName = TemplatesDir + sep + "hpa.yaml" - // NotesName is the name of the example NOTES.txt file. - NotesName = TemplatesDir + sep + "NOTES.txt" - // HelpersName is the name of the example helpers file. - HelpersName = TemplatesDir + sep + "_helpers.tpl" - // TestConnectionName is the name of the example test file. - TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml" -) - -// maxChartNameLength is lower than the limits we know of with certain file systems, -// and with certain Kubernetes fields. -const maxChartNameLength = 250 - -const sep = string(filepath.Separator) - -const defaultChartfile = `apiVersion: v2 -name: %s -description: A Helm chart for Kubernetes - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.16.0" -` - -const defaultValues = `# Default values for %s. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 1 - -image: - repository: nginx - pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart appVersion. - tag: "" - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: true - # Automatically mount a ServiceAccount's API credentials? - automount: true - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} -podLabels: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -service: - type: ClusterIP - port: 80 - -ingress: - enabled: false - className: "" - annotations: {} - # kubernetes.io/ingress.class: nginx - # kubernetes.io/tls-acme: "true" - hosts: - - host: chart-example.local - paths: - - path: / - pathType: ImplementationSpecific - tls: [] - # - secretName: chart-example-tls - # hosts: - # - chart-example.local - -resources: {} - # We usually recommend not to specify default resources and to leave this as a conscious - # choice for the user. This also increases chances charts run on environments with little - # resources, such as Minikube. If you do want to specify resources, uncomment the following - # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - # limits: - # cpu: 100m - # memory: 128Mi - # requests: - # cpu: 100m - # memory: 128Mi - -livenessProbe: - httpGet: - path: / - port: http -readinessProbe: - httpGet: - path: / - port: http - -autoscaling: - enabled: false - minReplicas: 1 - maxReplicas: 100 - targetCPUUtilizationPercentage: 80 - # targetMemoryUtilizationPercentage: 80 - -# Additional volumes on the output Deployment definition. -volumes: [] -# - name: foo -# secret: -# secretName: mysecret -# optional: false - -# Additional volumeMounts on the output Deployment definition. -volumeMounts: [] -# - name: foo -# mountPath: "/etc/foo" -# readOnly: true - -nodeSelector: {} - -tolerations: [] - -affinity: {} -` - -const defaultIgnore = `# Patterns to ignore when building packages. -# This supports shell glob matching, relative path matching, and -# negation (prefixed with !). Only one pattern per line. -.DS_Store -# Common VCS dirs -.git/ -.gitignore -.bzr/ -.bzrignore -.hg/ -.hgignore -.svn/ -# Common backup files -*.swp -*.bak -*.tmp -*.orig -*~ -# Various IDEs -.project -.idea/ -*.tmproj -.vscode/ -` - -const defaultIngress = `{{- if .Values.ingress.enabled -}} -{{- $fullName := include ".fullname" . -}} -{{- $svcPort := .Values.service.port -}} -{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} - {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} - {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} - {{- end }} -{{- end }} -{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1 -{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} -apiVersion: networking.k8s.io/v1beta1 -{{- else -}} -apiVersion: extensions/v1beta1 -{{- end }} -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include ".labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} - ingressClassName: {{ .Values.ingress.className }} - {{- end }} - {{- if .Values.ingress.tls }} - tls: - {{- range .Values.ingress.tls }} - - hosts: - {{- range .hosts }} - - {{ . | quote }} - {{- end }} - secretName: {{ .secretName }} - {{- end }} - {{- end }} - rules: - {{- range .Values.ingress.hosts }} - - host: {{ .host | quote }} - http: - paths: - {{- range .paths }} - - path: {{ .path }} - {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} - pathType: {{ .pathType }} - {{- end }} - backend: - {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} - service: - name: {{ $fullName }} - port: - number: {{ $svcPort }} - {{- else }} - serviceName: {{ $fullName }} - servicePort: {{ $svcPort }} - {{- end }} - {{- end }} - {{- end }} -{{- end }} -` - -const defaultDeployment = `apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include ".fullname" . }} - labels: - {{- include ".labels" . | nindent 4 }} -spec: - {{- if not .Values.autoscaling.enabled }} - replicas: {{ .Values.replicaCount }} - {{- end }} - selector: - matchLabels: - {{- include ".selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include ".labels" . | nindent 8 }} - {{- with .Values.podLabels }} - {{- toYaml . | nindent 8 }} - {{- end }} - spec: - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include ".serviceAccountName" . }} - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - containers: - - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - {{- toYaml .Values.livenessProbe | nindent 12 }} - readinessProbe: - {{- toYaml .Values.readinessProbe | nindent 12 }} - resources: - {{- toYaml .Values.resources | nindent 12 }} - {{- with .Values.volumeMounts }} - volumeMounts: - {{- toYaml . | nindent 12 }} - {{- end }} - {{- with .Values.volumes }} - volumes: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.affinity }} - affinity: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} -` - -const defaultService = `apiVersion: v1 -kind: Service -metadata: - name: {{ include ".fullname" . }} - labels: - {{- include ".labels" . | nindent 4 }} -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.port }} - targetPort: http - protocol: TCP - name: http - selector: - {{- include ".selectorLabels" . | nindent 4 }} -` - -const defaultServiceAccount = `{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include ".serviceAccountName" . }} - labels: - {{- include ".labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -automountServiceAccountToken: {{ .Values.serviceAccount.automount }} -{{- end }} -` - -const defaultHorizontalPodAutoscaler = `{{- if .Values.autoscaling.enabled }} -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: {{ include ".fullname" . }} - labels: - {{- include ".labels" . | nindent 4 }} -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: {{ include ".fullname" . }} - minReplicas: {{ .Values.autoscaling.minReplicas }} - maxReplicas: {{ .Values.autoscaling.maxReplicas }} - metrics: - {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} - {{- end }} - {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} - {{- end }} -{{- end }} -` - -const defaultNotes = `1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include ".fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include ".fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include ".fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include ".name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} -` - -const defaultHelpers = `{{/* -Expand the name of the chart. -*/}} -{{- define ".name" -}} -{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Create a default fully qualified app name. -We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). -If release name contains chart name it will be used as a full name. -*/}} -{{- define ".fullname" -}} -{{- if .Values.fullnameOverride }} -{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- $name := default .Chart.Name .Values.nameOverride }} -{{- if contains $name .Release.Name }} -{{- .Release.Name | trunc 63 | trimSuffix "-" }} -{{- else }} -{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} -{{- end }} -{{- end }} -{{- end }} - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define ".chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define ".labels" -}} -helm.sh/chart: {{ include ".chart" . }} -{{ include ".selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define ".selectorLabels" -}} -app.kubernetes.io/name: {{ include ".name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define ".serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include ".fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} -` - -const defaultTestConnection = `apiVersion: v1 -kind: Pod -metadata: - name: "{{ include ".fullname" . }}-test-connection" - labels: - {{- include ".labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include ".fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never -` - -// Stderr is an io.Writer to which error messages can be written -// -// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward -// compatibility. -var Stderr io.Writer = os.Stderr - -// CreateFrom creates a new chart, but scaffolds it from the src chart. -func CreateFrom(chartfile *chart.Metadata, dest, src string, opts helmopts.HelmOptions) error { - schart, err := loader.Load(src, opts) - if err != nil { - return errors.Wrapf(err, "could not load %s", src) - } - - schart.Metadata = chartfile - - var updatedTemplates []*chart.File - - for _, template := range schart.Templates { - newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &chart.File{Name: template.Name, Data: newData}) - } - - schart.Templates = updatedTemplates - b, err := yaml.Marshal(schart.Values) - if err != nil { - return errors.Wrap(err, "reading values file") - } - - var m map[string]interface{} - if err := yaml.Unmarshal(transform(string(b), schart.Name()), &m); err != nil { - return errors.Wrap(err, "transforming values file") - } - schart.Values = m - - // SaveDir looks for the file values.yaml when saving rather than the values - // key in order to preserve the comments in the YAML. The name placeholder - // needs to be replaced on that file. - for _, f := range schart.Raw { - if f.Name == ValuesfileName { - f.Data = transform(string(f.Data), schart.Name()) - } - } - - return SaveDir(schart, dest) -} - -// Create creates a new chart in a directory. -// -// Inside of dir, this will create a directory based on the name of -// chartfile.Name. It will then write the Chart.yaml into this directory and -// create the (empty) appropriate directories. -// -// The returned string will point to the newly created directory. It will be -// an absolute path, even if the provided base directory was relative. -// -// If dir does not exist, this will return an error. -// If Chart.yaml or any directories cannot be created, this will return an -// error. In such a case, this will attempt to clean up by removing the -// new chart directory. -func Create(name, dir string) (string, error) { - - // Sanity-check the name of a chart so user doesn't create one that causes problems. - if err := validateChartName(name); err != nil { - return "", err - } - - path, err := filepath.Abs(dir) - if err != nil { - return path, err - } - - if fi, err := os.Stat(path); err != nil { - return path, err - } else if !fi.IsDir() { - return path, errors.Errorf("no such directory %s", path) - } - - cdir := filepath.Join(path, name) - if fi, err := os.Stat(cdir); err == nil && !fi.IsDir() { - return cdir, errors.Errorf("file %s already exists and is not a directory", cdir) - } - - files := []struct { - path string - content []byte - }{ - { - // Chart.yaml - path: filepath.Join(cdir, ChartfileName), - content: []byte(fmt.Sprintf(defaultChartfile, name)), - }, - { - // values.yaml - path: filepath.Join(cdir, ValuesfileName), - content: []byte(fmt.Sprintf(defaultValues, name)), - }, - { - // .helmignore - path: filepath.Join(cdir, IgnorefileName), - content: []byte(defaultIgnore), - }, - { - // ingress.yaml - path: filepath.Join(cdir, IngressFileName), - content: transform(defaultIngress, name), - }, - { - // deployment.yaml - path: filepath.Join(cdir, DeploymentName), - content: transform(defaultDeployment, name), - }, - { - // service.yaml - path: filepath.Join(cdir, ServiceName), - content: transform(defaultService, name), - }, - { - // serviceaccount.yaml - path: filepath.Join(cdir, ServiceAccountName), - content: transform(defaultServiceAccount, name), - }, - { - // hpa.yaml - path: filepath.Join(cdir, HorizontalPodAutoscalerName), - content: transform(defaultHorizontalPodAutoscaler, name), - }, - { - // NOTES.txt - path: filepath.Join(cdir, NotesName), - content: transform(defaultNotes, name), - }, - { - // _helpers.tpl - path: filepath.Join(cdir, HelpersName), - content: transform(defaultHelpers, name), - }, - { - // test-connection.yaml - path: filepath.Join(cdir, TestConnectionName), - content: transform(defaultTestConnection, name), - }, - } - - for _, file := range files { - if _, err := os.Stat(file.path); err == nil { - // There is no handle to a preferred output stream here. - fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path) - } - if err := writeFile(file.path, file.content); err != nil { - return cdir, err - } - } - // Need to add the ChartsDir explicitly as it does not contain any file OOTB - if err := os.MkdirAll(filepath.Join(cdir, ChartsDir), 0755); err != nil { - return cdir, err - } - return cdir, nil -} - -// transform performs a string replacement of the specified source for -// a given key with the replacement string -func transform(src, replacement string) []byte { - return []byte(strings.ReplaceAll(src, "", replacement)) -} - -func writeFile(name string, content []byte) error { - if err := os.MkdirAll(filepath.Dir(name), 0755); err != nil { - return err - } - return os.WriteFile(name, content, 0644) -} - -func validateChartName(name string) error { - if name == "" || len(name) > maxChartNameLength { - return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength) - } - if !chartName.MatchString(name) { - return fmt.Errorf("chart name must match the regular expression %q", chartName.String()) - } - return nil -} diff --git a/pkg/helm/pkg/chartutil/create_test.go b/pkg/helm/pkg/chartutil/create_test.go deleted file mode 100644 index b4981a99..00000000 --- a/pkg/helm/pkg/chartutil/create_test.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "bytes" - "os" - "path/filepath" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" -) - -func TestCreate(t *testing.T) { - tdir := t.TempDir() - - c, err := Create("foo", tdir) - if err != nil { - t.Fatal(err) - } - - dir := filepath.Join(tdir, "foo") - - mychart, err := loader.LoadDir(c) - if err != nil { - t.Fatalf("Failed to load newly created chart %q: %s", c, err) - } - - if mychart.Name() != "foo" { - t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) - } - - for _, f := range []string{ - ChartfileName, - DeploymentName, - HelpersName, - IgnorefileName, - NotesName, - ServiceAccountName, - ServiceName, - TemplatesDir, - TemplatesTestsDir, - TestConnectionName, - ValuesfileName, - } { - if _, err := os.Stat(filepath.Join(dir, f)); err != nil { - t.Errorf("Expected %s file: %s", f, err) - } - } -} - -func TestCreateFrom(t *testing.T) { - tdir := t.TempDir() - - cf := &chart.Metadata{ - APIVersion: chart.APIVersionV1, - Name: "foo", - Version: "0.1.0", - } - srcdir := "./testdata/frobnitz/charts/mariner" - - if err := CreateFrom(cf, tdir, srcdir); err != nil { - t.Fatal(err) - } - - dir := filepath.Join(tdir, "foo") - c := filepath.Join(tdir, cf.Name) - mychart, err := loader.LoadDir(c) - if err != nil { - t.Fatalf("Failed to load newly created chart %q: %s", c, err) - } - - if mychart.Name() != "foo" { - t.Errorf("Expected name to be 'foo', got %q", mychart.Name()) - } - - for _, f := range []string{ - ChartfileName, - ValuesfileName, - filepath.Join(TemplatesDir, "placeholder.tpl"), - } { - if _, err := os.Stat(filepath.Join(dir, f)); err != nil { - t.Errorf("Expected %s file: %s", f, err) - } - - // Check each file to make sure has been replaced - b, err := os.ReadFile(filepath.Join(dir, f)) - if err != nil { - t.Errorf("Unable to read file %s: %s", f, err) - } - if bytes.Contains(b, []byte("")) { - t.Errorf("File %s contains ", f) - } - } -} - -// TestCreate_Overwrite is a regression test for making sure that files are overwritten. -func TestCreate_Overwrite(t *testing.T) { - tdir := t.TempDir() - - var errlog bytes.Buffer - - if _, err := Create("foo", tdir); err != nil { - t.Fatal(err) - } - - dir := filepath.Join(tdir, "foo") - - tplname := filepath.Join(dir, "templates/hpa.yaml") - writeFile(tplname, []byte("FOO")) - - // Now re-run the create - Stderr = &errlog - if _, err := Create("foo", tdir); err != nil { - t.Fatal(err) - } - - data, err := os.ReadFile(tplname) - if err != nil { - t.Fatal(err) - } - - if string(data) == "FOO" { - t.Fatal("File that should have been modified was not.") - } - - if errlog.Len() == 0 { - t.Errorf("Expected warnings about overwriting files.") - } -} - -func TestValidateChartName(t *testing.T) { - for name, shouldPass := range map[string]bool{ - "": false, - "abcdefghijklmnopqrstuvwxyz-_.": true, - "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true, - "$hello": false, - "Hellô": false, - "he%%o": false, - "he\nllo": false, - - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "abcdefghijklmnopqrstuvwxyz-_." + - "ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false, - } { - if err := validateChartName(name); (err != nil) == shouldPass { - t.Errorf("test for %q failed", name) - } - } -} diff --git a/pkg/helm/pkg/chartutil/dependencies.go b/pkg/helm/pkg/chartutil/dependencies.go deleted file mode 100644 index 41d86d27..00000000 --- a/pkg/helm/pkg/chartutil/dependencies.go +++ /dev/null @@ -1,736 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "errors" - "fmt" - "log" - "strings" - - "github.com/mitchellh/copystructure" - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -// ProcessDependencies checks through this chart's dependencies, processing accordingly. -// -// TODO: For Helm v4 this can be combined with or turned into ProcessDependenciesWithMerge -func ProcessDependencies(c *chart.Chart, v *map[string]interface{}) error { - if err := processDependencyExportExtraValues(c, v, false); err != nil { - return err - } - - if err := processDependencyEnabled(c, *v, ""); err != nil { - return err - } - - return processDependencyImportExportValues(c, false) -} - -// ProcessDependenciesWithMerge checks through this chart's dependencies, processing accordingly. -// It is similar to ProcessDependencies but it does not remove nil values during -// the import/export handling process. -func ProcessDependenciesWithMerge(c *chart.Chart, v *map[string]interface{}) error { - if err := processDependencyExportExtraValues(c, v, true); err != nil { - return err - } - - if err := processDependencyEnabled(c, *v, ""); err != nil { - return err - } - - return processDependencyImportExportValues(c, true) -} - -// processDependencyConditions disables charts based on condition path value in values -func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath string) { - if reqs == nil { - return - } - for _, r := range reqs { - for _, c := range strings.Split(strings.TrimSpace(r.Condition), ",") { - if len(c) > 0 { - // retrieve value - vv, err := cvals.PathValue(cpath + c) - if err == nil { - // if not bool, warn - if bv, ok := vv.(bool); ok { - r.Enabled = bv - break - } - log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name) - } else if _, ok := err.(ErrNoValue); !ok { - // this is a real error - log.Printf("Warning: PathValue returned error %v", err) - } - } - } - } -} - -// processDependencyTags disables charts based on tags in values -func processDependencyTags(reqs []*chart.Dependency, cvals Values) { - if reqs == nil { - return - } - vt, err := cvals.Table("tags") - if err != nil { - return - } - for _, r := range reqs { - var hasTrue, hasFalse bool - for _, k := range r.Tags { - if b, ok := vt[k]; ok { - // if not bool, warn - if bv, ok := b.(bool); ok { - if bv { - hasTrue = true - } else { - hasFalse = true - } - } else { - log.Printf("Warning: Tag '%s' for chart %s returned non-bool value", k, r.Name) - } - } - } - if !hasTrue && hasFalse { - r.Enabled = false - } else if hasTrue || !hasTrue && !hasFalse { - r.Enabled = true - } - } -} - -func getAliasDependency(charts []*chart.Chart, dep *chart.Dependency) *chart.Chart { - for _, c := range charts { - if c == nil { - continue - } - if c.Name() != dep.Name { - continue - } - if !IsCompatibleRange(dep.Version, c.Metadata.Version) { - continue - } - - out := *c - md := *c.Metadata - out.Metadata = &md - - if dep.Alias != "" { - md.Name = dep.Alias - } - return &out - } - return nil -} - -// processDependencyEnabled removes disabled charts from dependencies -func processDependencyEnabled(c *chart.Chart, v map[string]interface{}, path string) error { - if c.Metadata.Dependencies == nil { - return nil - } - - var chartDependencies []*chart.Chart - // If any dependency is not a part of Chart.yaml - // then this should be added to chartDependencies. - // However, if the dependency is already specified in Chart.yaml - // we should not add it, as it would be anyways processed from Chart.yaml - -Loop: - for _, existing := range c.Dependencies() { - for _, req := range c.Metadata.Dependencies { - if existing.Name() == req.Name && IsCompatibleRange(req.Version, existing.Metadata.Version) { - continue Loop - } - } - chartDependencies = append(chartDependencies, existing) - } - - for _, req := range c.Metadata.Dependencies { - if req == nil { - continue - } - if chartDependency := getAliasDependency(c.Dependencies(), req); chartDependency != nil { - chartDependencies = append(chartDependencies, chartDependency) - } - if req.Alias != "" { - req.Name = req.Alias - } - } - c.SetDependencies(chartDependencies...) - - // set all to true - for _, lr := range c.Metadata.Dependencies { - lr.Enabled = true - } - cvals, err := CoalesceValues(c, v) - if err != nil { - return err - } - // flag dependencies as enabled/disabled - processDependencyTags(c.Metadata.Dependencies, cvals) - processDependencyConditions(c.Metadata.Dependencies, cvals, path) - // make a map of charts to remove - rm := map[string]struct{}{} - for _, r := range c.Metadata.Dependencies { - if !r.Enabled { - // remove disabled chart - rm[r.Name] = struct{}{} - } - } - // don't keep disabled charts in new slice - cd := []*chart.Chart{} - copy(cd, c.Dependencies()[:0]) - for _, n := range c.Dependencies() { - if _, ok := rm[n.Metadata.Name]; !ok { - cd = append(cd, n) - } - } - // don't keep disabled charts in metadata - cdMetadata := []*chart.Dependency{} - copy(cdMetadata, c.Metadata.Dependencies[:0]) - for _, n := range c.Metadata.Dependencies { - if _, ok := rm[n.Name]; !ok { - cdMetadata = append(cdMetadata, n) - } - } - - // recursively call self to process sub dependencies - for _, t := range cd { - subpath := path + t.Metadata.Name + "." - if err := processDependencyEnabled(t, cvals, subpath); err != nil { - return err - } - } - // set the correct dependencies in metadata - c.Metadata.Dependencies = nil - c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...) - c.SetDependencies(cd...) - - return nil -} - -// pathToMap creates a nested map given a YAML path in dot notation. -func pathToMap(path string, data map[string]interface{}) map[string]interface{} { - if path == "." { - return data - } - return set(parsePath(path), data) -} - -func set(path []string, data map[string]interface{}) map[string]interface{} { - if len(path) == 0 { - return nil - } - cur := data - for i := len(path) - 1; i >= 0; i-- { - cur = map[string]interface{}{path[i]: cur} - } - return cur -} - -// processImportValues merges values from child to parent based on the chart's dependencies' ImportValues field. -func processImportValues(c *chart.Chart, merge bool) error { - if c.Metadata.Dependencies == nil { - return nil - } - // combine chart values and empty config to get Values - var cvals Values - var err error - if merge { - cvals, err = MergeValues(c, nil) - } else { - cvals, err = CoalesceValues(c, nil) - } - if err != nil { - return err - } - b := make(map[string]interface{}) - // import values from each dependency if specified in import-values - for _, r := range c.Metadata.Dependencies { - var outiv []interface{} - for _, riv := range r.ImportValues { - switch iv := riv.(type) { - case map[string]interface{}: - child := iv["child"].(string) - parent := iv["parent"].(string) - - outiv = append(outiv, map[string]string{ - "child": child, - "parent": parent, - }) - - // get child table - vv, err := cvals.Table(r.Name + "." + child) - if err != nil { - log.Printf("Warning: ImportValues missing table from chart %s: %v", r.Name, err) - continue - } - // create value map from child to be merged into parent - if merge { - b = MergeTables(b, pathToMap(parent, vv.AsMap())) - } else { - b = CoalesceTables(b, pathToMap(parent, vv.AsMap())) - } - case string: - child := "exports." + iv - outiv = append(outiv, map[string]string{ - "child": child, - "parent": ".", - }) - vm, err := cvals.Table(r.Name + "." + child) - if err != nil { - log.Printf("Warning: ImportValues missing table: %v", err) - continue - } - if merge { - b = MergeTables(b, vm.AsMap()) - } else { - b = CoalesceTables(b, vm.AsMap()) - } - } - } - r.ImportValues = outiv - } - - // Imported values from a child to a parent chart have a lower priority than - // the parents values. This enables parent charts to import a large section - // from a child and then override select parts. This is why b is merged into - // cvals in the code below and not the other way around. - if merge { - // deep copying the cvals as there are cases where pointers can end - // up in the cvals when they are copied onto b in ways that break things. - cvals = deepCopyMap(cvals) - c.Values = MergeTables(cvals, b) - } else { - // Trimming the nil values from cvals is needed for backwards compatibility. - // Previously, the b value had been populated with cvals along with some - // overrides. This caused the coalescing functionality to remove the - // nil/null values. This trimming is for backwards compat. - cvals = trimNilValues(cvals) - c.Values = CoalesceTables(cvals, b) - } - - return nil -} - -func deepCopyMap(vals map[string]interface{}) map[string]interface{} { - valsCopy, err := copystructure.Copy(vals) - if err != nil { - return vals - } - return valsCopy.(map[string]interface{}) -} - -func trimNilValues(vals map[string]interface{}) map[string]interface{} { - valsCopy, err := copystructure.Copy(vals) - if err != nil { - return vals - } - valsCopyMap := valsCopy.(map[string]interface{}) - for key, val := range valsCopyMap { - if val == nil { - // Iterate over the values and remove nil keys - delete(valsCopyMap, key) - } else if istable(val) { - // Recursively call into ourselves to remove keys from inner tables - valsCopyMap[key] = trimNilValues(val.(map[string]interface{})) - } - } - - return valsCopyMap -} - -// processDependencyImportValues imports specified chart values from child to parent. -func processDependencyImportValues(c *chart.Chart, merge bool) error { - for _, d := range c.Dependencies() { - // recurse - if err := processDependencyImportValues(d, merge); err != nil { - return err - } - } - return processImportValues(c, merge) -} - -// Extend Chart Values according to export-values directive of its parent Chart. -func processExportValues(c *chart.Chart, merge bool) error { - if c.Parent() == nil || c.Parent().Metadata.Dependencies == nil { - return nil - } - - // Get current chart as chart.Dependency object. - var cr *chart.Dependency - for _, r := range c.Parent().Metadata.Dependencies { - if r.Name == c.Name() { - cr = r - break - } - } - - if cr == nil { - return nil - } - - // Get parent chart values. - var pvals Values - var err error - if merge { - pvals, err = MergeValues(c.Parent(), nil) - if err != nil { - return err - } - } else { - pvals, err = CoalesceValues(c.Parent(), nil) - if err != nil { - return err - } - } - - // Get current chart values. - var cvals Values - if merge { - cvals, err = MergeValues(c, nil) - if err != nil { - return err - } - } else { - cvals, err = CoalesceValues(c, nil) - if err != nil { - return err - } - } - - // Generate Values map to be merged into current chart, according to export-values directive. - exportedValues, err := getExportedValues(c.Parent().Name(), cr, pvals, merge) - if err != nil { - return err - } - - cv, err := copystructure.Copy(cvals) - if err != nil { - return err - } - - ev, err := copystructure.Copy(exportedValues) - if err != nil { - return err - } - - // Merge newly generated extra Values map into current chart Values. - if merge { - c.Values = MergeTables(ev.(map[string]interface{}), cv.(Values)) - } else { - c.Values = CoalesceTables(ev.(map[string]interface{}), cv.(Values)) - } - - evForSync, err := copystructure.Copy(exportedValues) - if err != nil { - return err - } - - // Make sure no parent chart will override our new extra Values in this chart. - if err := syncChartOverridesToParentsValues(c, evForSync.(map[string]interface{}), merge); err != nil { - return err - } - - return nil -} - -// Get Values map with overrides destined for current chart and merge these overrides into all its parent charts -// Values, while prefixing the to be applied parent overrides with the relative path to the current chart. This is -// to avoid values from parent charts having precedence to the overrides passed to the current chart. -func syncChartOverridesToParentsValues(c *chart.Chart, overrides map[string]interface{}, merge bool) error { - if c.Parent() == nil { - return nil - } - - // Get parent chart values. - var pvals Values - var err error - if merge { - pvals, err = MergeValues(c.Parent(), nil) - if err != nil { - return err - } - } else { - pvals, err = CoalesceValues(c.Parent(), nil) - if err != nil { - return err - } - } - - pv, err := copystructure.Copy(pvals) - if err != nil { - return err - } - - o, err := copystructure.Copy(overrides) - if err != nil { - return err - } - - parentOverrides := pathToMap(c.Name(), o.(map[string]interface{})) - - po, err := copystructure.Copy(parentOverrides) - if err != nil { - return err - } - - if merge { - c.Parent().Values = MergeTables(po.(map[string]interface{}), pv.(Values)) - } else { - c.Parent().Values = CoalesceTables(po.(map[string]interface{}), pv.(Values)) - } - - return syncChartOverridesToParentsValues(c.Parent(), parentOverrides, merge) -} - -// Extend extra Values overrides according to export-values directive, if needed. -func processExportExtraValues(c *chart.Chart, extraVals *map[string]interface{}, merge bool) error { - if c.Parent() == nil || c.Parent().Metadata.Dependencies == nil { - return nil - } - - // Get current Chart as chart.Dependency. - var cr *chart.Dependency - for _, r := range c.Parent().Metadata.Dependencies { - if r.Name == c.Name() { - cr = r - break - } - } - - if cr == nil { - return nil - } - - for _, exportValue := range cr.ExportValues { - parent, child, err := parseExportValues(exportValue) - if err != nil { - log.Printf("Warning: invalid ExportValues defined in chart %q for its dependency %q: %s", c.Parent().Name(), cr.Name, err) - continue - } - - headlessParentChartPath := stripFirstPathPart(c.Parent().ChartPath()) - var exportParentTablePath string - if headlessParentChartPath != "" { - exportParentTablePath = joinPath(headlessParentChartPath, parent) - } else { - exportParentTablePath = parent - } - - // If present, get extra Values overrides table from parent path, as defined in export-values. - extraParentVals, err := Values(*extraVals).Table(exportParentTablePath) - if err != nil { - var errNoTable ErrNoTable - if errors.As(err, &errNoTable) { - continue - } else { - return err - } - } - - var extraChildValsPath string - if child != "" { - extraChildValsPath = joinPath(stripFirstPathPart(c.ChartPath()), child) - } else { - extraChildValsPath = stripFirstPathPart(c.ChartPath()) - } - - // Do not overwrite anything — skip if something present in destination. - var errNoTable ErrNoTable - var errNoValue ErrNoValue - _, errTable := Values(*extraVals).Table(extraChildValsPath) - _, errValue := Values(*extraVals).PathValue(extraChildValsPath) - if !(errors.As(errTable, &errNoTable) && errors.As(errValue, &errNoValue)) { - continue - } - - // Create new Values map structure to be merged into extra Values overrides map. - extraChildVals, err := copystructure.Copy(pathToMap(extraChildValsPath, extraParentVals.AsMap())) - if err != nil { - return err - } - - // Merge new Values into existing extra Values overrides. - if merge { - *extraVals = MergeTables(extraChildVals.(map[string]interface{}), *extraVals) - } else { - *extraVals = CoalesceTables(extraChildVals.(map[string]interface{}), *extraVals) - } - } - - return nil -} - -// Generate Values map to be merged into child chart, according to export-values directive of parent chart. -func getExportedValues(parentName string, r *chart.Dependency, pvals Values, merge bool) (map[string]interface{}, error) { - b := make(map[string]interface{}) - var exportValues []interface{} - for _, rev := range r.ExportValues { - parent, child, err := parseExportValues(rev) - if err != nil { - log.Printf("Warning: invalid ExportValues defined in chart %q for its dependency %q: %s", parentName, r.Name, err) - continue - } - - exportValues = append(exportValues, map[string]string{ - "parent": parent, - "child": child, - }) - - var childValMap map[string]interface{} - // Try to get parent table for parent path specified in export-values. - vm, err := pvals.Table(parent) - if err == nil { - // It IS a valid table. - if child == "" { - childValMap = vm.AsMap() - } else { - childValMap = pathToMap(child, vm.AsMap()) - } - } else { - // If it's not a table, it might be a simple value. - value, e := pvals.PathValue(parent) - if e != nil { - log.Printf("Warning: ExportValues defined in chart %q for its dependency %q can't get the parent path: %s", parentName, r.Name, err.Error()) - continue - } - - childSlice := parsePath(child) - if len(childSlice) == 1 && childSlice[0] == "" { - log.Printf("Warning: in ExportValues defined in chart %q for its dependency %q you are trying to assign a primitive data type (string, int, etc) to the root of your dependent chart values. We will ignore this ExportValues, because this is most likely not what you want. Fix the ExportValues to hide this warning.", parentName, r.Name) - continue - } - - childPath := joinPath(childSlice[:len(childSlice)-1]...) - childMap := map[string]interface{}{ - childSlice[len(childSlice)-1]: value, - } - - if childPath != "" { - childValMap = pathToMap(childPath, childMap) - } else { - childValMap = childMap - } - } - - chValMap, err := copystructure.Copy(childValMap) - if err != nil { - return b, err - } - - // Merge new Values map for current export-values directive into other new Values maps for other export-values directives. - if merge { - b = MergeTables(chValMap.(map[string]interface{}), b) - } else { - b = CoalesceTables(chValMap.(map[string]interface{}), b) - } - } - - // Set formatted export values. - r.ExportValues = exportValues - - return b, nil -} - -// Parse and validate export-values. -func parseExportValues(rev interface{}) (string, string, error) { - var parent, child string - - switch ev := rev.(type) { - case map[string]interface{}: - var ok bool - parent, ok = ev["parent"].(string) - if !ok { - return "", "", fmt.Errorf("parent must be a string") - } - - child, ok = ev["child"].(string) - if !ok { - return "", "", fmt.Errorf("child must be a string") - } - - if strings.TrimSpace(parent) == "" || strings.TrimSpace(parent) == "." { - return "", "", fmt.Errorf("parent %q is not allowed", parent) - } - - parent = strings.TrimSpace(parent) - child = strings.TrimSpace(child) - - if child == "." { - child = "" - } - case string: - switch parent = strings.TrimSpace(ev); parent { - case "", ".": - parent = "exports" - default: - parent = "exports." + parent - } - child = "" - default: - return "", "", fmt.Errorf("invalid format of ExportValues") - } - - return parent, child, nil -} - -func processDependencyImportExportValues(c *chart.Chart, merge bool) error { - if err := processDependencyExportValues(c, merge); err != nil { - return err - } - - return processDependencyImportValues(c, merge) -} - -// Update Values of Chart and its Dependencies according to export-values directive. -func processDependencyExportValues(c *chart.Chart, merge bool) error { - if err := processExportValues(c, merge); err != nil { - return err - } - - for _, d := range c.Dependencies() { - // recurse - if err := processDependencyExportValues(d, merge); err != nil { - return err - } - } - - return nil -} - -// Update extra Values overrides according to export-values directive, if needed. -func processDependencyExportExtraValues(c *chart.Chart, extraVals *map[string]interface{}, merge bool) error { - if err := processExportExtraValues(c, extraVals, merge); err != nil { - return err - } - - for _, d := range c.Dependencies() { - // recurse - if err := processDependencyExportExtraValues(d, extraVals, merge); err != nil { - return err - } - } - - return nil -} - -func stripFirstPathPart(path string) string { - pathParts := parsePath(path)[1:] - return joinPath(pathParts...) -} diff --git a/pkg/helm/pkg/chartutil/dependencies_test.go b/pkg/helm/pkg/chartutil/dependencies_test.go deleted file mode 100644 index c198c3ba..00000000 --- a/pkg/helm/pkg/chartutil/dependencies_test.go +++ /dev/null @@ -1,572 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package chartutil - -import ( - "os" - "path/filepath" - "sort" - "strconv" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" -) - -func loadChart(t *testing.T, path string) *chart.Chart { - t.Helper() - c, err := loader.Load(path) - if err != nil { - t.Fatalf("failed to load testdata: %s", err) - } - return c -} - -func TestLoadDependency(t *testing.T) { - tests := []*chart.Dependency{ - {Name: "alpine", Version: "0.1.0", Repository: "https://example.com/charts"}, - {Name: "mariner", Version: "4.3.2", Repository: "https://example.com/charts"}, - } - - check := func(deps []*chart.Dependency) { - if len(deps) != 2 { - t.Errorf("expected 2 dependencies, got %d", len(deps)) - } - for i, tt := range tests { - if deps[i].Name != tt.Name { - t.Errorf("expected dependency named %q, got %q", tt.Name, deps[i].Name) - } - if deps[i].Version != tt.Version { - t.Errorf("expected dependency named %q to have version %q, got %q", tt.Name, tt.Version, deps[i].Version) - } - if deps[i].Repository != tt.Repository { - t.Errorf("expected dependency named %q to have repository %q, got %q", tt.Name, tt.Repository, deps[i].Repository) - } - } - } - c := loadChart(t, "testdata/frobnitz") - check(c.Metadata.Dependencies) - check(c.Lock.Dependencies) -} - -func TestDependencyEnabled(t *testing.T) { - type M = map[string]interface{} - tests := []struct { - name string - v M - e []string // expected charts including duplicates in alphanumeric order - }{{ - "tags with no effect", - M{"tags": M{"nothinguseful": false}}, - []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, - }, { - "tags disabling a group", - M{"tags": M{"front-end": false}}, - []string{"parentchart"}, - }, { - "tags disabling a group and enabling a different group", - M{"tags": M{"front-end": false, "back-end": true}}, - []string{"parentchart", "parentchart.subchart2", "parentchart.subchart2.subchartb", "parentchart.subchart2.subchartc"}, - }, { - "tags disabling only children, children still enabled since tag front-end=true in values.yaml", - M{"tags": M{"subcharta": false, "subchartb": false}}, - []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb"}, - }, { - "tags disabling all parents/children with additional tag re-enabling a parent", - M{"tags": M{"front-end": false, "subchart1": true, "back-end": false}}, - []string{"parentchart", "parentchart.subchart1"}, - }, { - "conditions enabling the parent charts, but back-end (b, c) is still disabled via values.yaml", - M{"subchart1": M{"enabled": true}, "subchart2": M{"enabled": true}}, - []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2"}, - }, { - "conditions disabling the parent charts, effectively disabling children", - M{"subchart1": M{"enabled": false}, "subchart2": M{"enabled": false}}, - []string{"parentchart"}, - }, { - "conditions a child using the second condition path of child's condition", - M{"subchart1": M{"subcharta": M{"enabled": false}}}, - []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subchartb"}, - }, { - "tags enabling a parent/child group with condition disabling one child", - M{"subchart2": M{"subchartc": M{"enabled": false}}, "tags": M{"back-end": true}}, - []string{"parentchart", "parentchart.subchart1", "parentchart.subchart1.subcharta", "parentchart.subchart1.subchartb", "parentchart.subchart2", "parentchart.subchart2.subchartb"}, - }, { - "tags will not enable a child if parent is explicitly disabled with condition", - M{"subchart1": M{"enabled": false}, "tags": M{"front-end": true}}, - []string{"parentchart"}, - }, { - "subcharts with alias also respect conditions", - M{"subchart1": M{"enabled": false}, "subchart2alias": M{"enabled": true, "subchartb": M{"enabled": true}}}, - []string{"parentchart", "parentchart.subchart2alias", "parentchart.subchart2alias.subchartb"}, - }} - - for _, tc := range tests { - c := loadChart(t, "testdata/subpop") - t.Run(tc.name, func(t *testing.T) { - if err := processDependencyEnabled(c, tc.v, ""); err != nil { - t.Fatalf("error processing enabled dependencies %v", err) - } - - names := extractChartNames(c) - if len(names) != len(tc.e) { - t.Fatalf("slice lengths do not match got %v, expected %v", len(names), len(tc.e)) - } - for i := range names { - if names[i] != tc.e[i] { - t.Fatalf("slice values do not match got %v, expected %v", names, tc.e) - } - } - }) - } -} - -// extractCharts recursively searches chart dependencies returning all charts found -func extractChartNames(c *chart.Chart) []string { - var out []string - var fn func(c *chart.Chart) - fn = func(c *chart.Chart) { - out = append(out, c.ChartPath()) - for _, d := range c.Dependencies() { - fn(d) - } - } - fn(c) - sort.Strings(out) - return out -} - -func TestProcessDependencyImportValues(t *testing.T) { - c := loadChart(t, "testdata/subpop") - - e := make(map[string]string) - - e["imported-chart1.SC1bool"] = "true" - e["imported-chart1.SC1float"] = "3.14" - e["imported-chart1.SC1int"] = "100" - e["imported-chart1.SC1string"] = "dollywood" - e["imported-chart1.SC1extra1"] = "11" - e["imported-chart1.SPextra1"] = "helm rocks" - e["imported-chart1.SC1extra1"] = "11" - - e["imported-chartA.SCAbool"] = "false" - e["imported-chartA.SCAfloat"] = "3.1" - e["imported-chartA.SCAint"] = "55" - e["imported-chartA.SCAstring"] = "jabba" - e["imported-chartA.SPextra3"] = "1.337" - e["imported-chartA.SC1extra2"] = "1.337" - e["imported-chartA.SCAnested1.SCAnested2"] = "true" - - e["imported-chartA-B.SCAbool"] = "false" - e["imported-chartA-B.SCAfloat"] = "3.1" - e["imported-chartA-B.SCAint"] = "55" - e["imported-chartA-B.SCAstring"] = "jabba" - - e["imported-chartA-B.SCBbool"] = "true" - e["imported-chartA-B.SCBfloat"] = "7.77" - e["imported-chartA-B.SCBint"] = "33" - e["imported-chartA-B.SCBstring"] = "boba" - e["imported-chartA-B.SPextra5"] = "k8s" - e["imported-chartA-B.SC1extra5"] = "tiller" - - // These values are imported from the child chart to the parent. Parent - // values take precedence over imported values. This enables importing a - // large section from a child chart and overriding a selection from it. - e["overridden-chart1.SC1bool"] = "false" - e["overridden-chart1.SC1float"] = "3.141592" - e["overridden-chart1.SC1int"] = "99" - e["overridden-chart1.SC1string"] = "pollywog" - e["overridden-chart1.SPextra2"] = "42" - - e["overridden-chartA.SCAbool"] = "true" - e["overridden-chartA.SCAfloat"] = "41.3" - e["overridden-chartA.SCAint"] = "808" - e["overridden-chartA.SCAstring"] = "jabberwocky" - e["overridden-chartA.SPextra4"] = "true" - - // These values are imported from the child chart to the parent. Parent - // values take precedence over imported values. This enables importing a - // large section from a child chart and overriding a selection from it. - e["overridden-chartA-B.SCAbool"] = "true" - e["overridden-chartA-B.SCAfloat"] = "41.3" - e["overridden-chartA-B.SCAint"] = "808" - e["overridden-chartA-B.SCAstring"] = "jabberwocky" - e["overridden-chartA-B.SCBbool"] = "false" - e["overridden-chartA-B.SCBfloat"] = "1.99" - e["overridden-chartA-B.SCBint"] = "77" - e["overridden-chartA-B.SCBstring"] = "jango" - e["overridden-chartA-B.SPextra6"] = "111" - e["overridden-chartA-B.SCAextra1"] = "23" - e["overridden-chartA-B.SCBextra1"] = "13" - e["overridden-chartA-B.SC1extra6"] = "77" - - // `exports` style - e["SCBexported1B"] = "1965" - e["SC1extra7"] = "true" - e["SCBexported2A"] = "blaster" - e["global.SC1exported2.all.SC1exported3"] = "SC1expstr" - - if err := processDependencyImportExportValues(c, false); err != nil { - t.Fatalf("processing import values dependencies %v", err) - } - cc := Values(c.Values) - for kk, vv := range e { - pv, err := cc.PathValue(kk) - if err != nil { - t.Fatalf("retrieving import values table %v %v", kk, err) - } - - switch pv := pv.(type) { - case float64: - if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { - t.Errorf("failed to match imported float value %v with expected %v for key %q", s, vv, kk) - } - case bool: - if b := strconv.FormatBool(pv); b != vv { - t.Errorf("failed to match imported bool value %v with expected %v for key %q", b, vv, kk) - } - default: - if pv != vv { - t.Errorf("failed to match imported string value %q with expected %q for key %q", pv, vv, kk) - } - } - } - - // Since this was processed with coalescing there should be no null values. - // Here we verify that. - _, err := cc.PathValue("ensurenull") - if err == nil { - t.Error("expect nil value not found but found it") - } - switch xerr := err.(type) { - case ErrNoValue: - // We found what we expected - default: - t.Errorf("expected an ErrNoValue but got %q instead", xerr) - } - - c = loadChart(t, "testdata/subpop") - if err := processDependencyImportValues(c, true); err != nil { - t.Fatalf("processing import values dependencies %v", err) - } - cc = Values(c.Values) - val, err := cc.PathValue("ensurenull") - if err != nil { - t.Error("expect value but ensurenull was not found") - } - if val != nil { - t.Errorf("expect nil value but got %q instead", val) - } -} - -func TestProcessDependencyImportValuesMultiLevelPrecedence(t *testing.T) { - c := loadChart(t, "testdata/three-level-dependent-chart/umbrella") - - e := make(map[string]string) - - // The order of precedence should be: - // 1. User specified values (e.g CLI) - // 2. Parent chart values - // 3. Imported values - // 4. Sub-chart values - // The 4 app charts here deal with things differently: - // - app1 has a port value set in the umbrella chart. It does not import any - // values so the value from the umbrella chart should be used. - // - app2 has a value in the app chart and imports from the library. The - // app chart value should take precedence. - // - app3 has no value in the app chart and imports the value from the library - // chart. The library chart value should be used. - // - app4 has a value in the app chart and does not import the value from the - // library chart. The app charts value should be used. - e["app1.service.port"] = "3456" - e["app2.service.port"] = "8080" - e["app3.service.port"] = "9090" - e["app4.service.port"] = "1234" - if err := processDependencyImportExportValues(c, true); err != nil { - t.Fatalf("processing import values dependencies %v", err) - } - cc := Values(c.Values) - for kk, vv := range e { - pv, err := cc.PathValue(kk) - if err != nil { - t.Fatalf("retrieving import values table %v %v", kk, err) - } - - switch pv := pv.(type) { - case float64: - if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { - t.Errorf("failed to match imported float value %v with expected %v", s, vv) - } - default: - if pv != vv { - t.Errorf("failed to match imported string value %q with expected %q", pv, vv) - } - } - } -} - -func TestProcessDependencyExportValues(t *testing.T) { - c := loadChart(t, "testdata/subpop") - - e := make(map[string]string) - - // merge with no overrides - e["subchart1.exported-parent.SPExtra7"] = "exported-from-parent" - e["subchart1.exported-parent.SPExtra10"] = "should-be-unchanged" - e["subchart1.exported-parent.SPNested1.SPExtra19"] = "exported-from-parent-n6" - - // single value export - e["subchart1.exported-single-value-parent"] = "exported-from-parent-n7" - - // merge with overrides - e["subchart1.exported-overridden-parent.SC1bool"] = "true" - e["subchart1.exported-overridden-parent.SC1float"] = "22.2" - e["subchart1.exported-overridden-parent.SC1int"] = "222" - e["subchart1.exported-overridden-parent.SC1string"] = "exported-from-parent-n2" - e["subchart1.exported-overridden-parent.SPExtra8"] = "exported-from-parent-n3" - e["subchart1.exported-overridden-parent.SPExtra11"] = "should-be-unchanged" - - // `exports` style, no overrides - e["subchart1.exported-short-parent.SPExtra9"] = "exported-from-parent-n4" - e["subchart1.exported-short-parent.SPExtra12"] = "should-be-unchanged" - - // passed from child to its own child with overrides - e["subchart1.subcharta.exported-overridden-chart1.SCAbool"] = "true" - e["subchart1.subcharta.exported-overridden-chart1.SCAfloat"] = "33.3" - e["subchart1.subcharta.exported-overridden-chart1.SCAint"] = "333" - e["subchart1.subcharta.exported-overridden-chart1.SCAstring"] = "exported-from-chart1" - e["subchart1.subcharta.exported-overridden-chart1.SPExtra13"] = "exported-from-chart1-n2" - e["subchart1.subcharta.exported-overridden-chart1.SPExtra15"] = "should-be-unchanged" - - // passed from child to its own child, `exports` style, no overrides - e["subchart1.subcharta.exported-short-chart1.SPExtra14"] = "exported-from-chart1-n3" - e["subchart1.subcharta.exported-short-chart1.SPExtra16"] = "should-be-unchanged" - - // passed through from parent to the child of the child chart, no overrides - e["subchart1.subcharta.exported-passthrough.SPExtra17"] = "exported-from-parent-n5" - e["subchart1.subcharta.exported-passthrough.SPExtra18"] = "should-be-unchanged" - - if err := processDependencyImportExportValues(c, true); err != nil { - t.Fatalf("processing export values dependencies %v", err) - } - cc := Values(c.Values) - for kk, vv := range e { - pv, err := cc.PathValue(kk) - if err != nil { - t.Errorf("retrieving export values table %v %v", kk, err) - } - - switch pv := pv.(type) { - case float64: - if s := strconv.FormatFloat(pv, 'f', -1, 64); s != vv { - t.Errorf("failed to match exported float value %v with expected %v", s, vv) - } - case bool: - if b := strconv.FormatBool(pv); b != vv { - t.Errorf("failed to match exported bool value %v with expected %v", b, vv) - } - default: - if pv != vv { - t.Errorf("failed to match exported string value %q with expected %q", pv, vv) - } - } - } -} - -func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) { - c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") - nameOverride := "parent-chart-prod" - - if err := processDependencyImportExportValues(c, true); err != nil { - t.Fatalf("processing import values dependencies %v", err) - } - - if len(c.Dependencies()) != 2 { - t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) - } - - if err := processDependencyEnabled(c, c.Values, ""); err != nil { - t.Fatalf("expected no errors but got %q", err) - } - - if len(c.Dependencies()) != 1 { - t.Fatal("expected no changes in dependencies") - } - - if len(c.Metadata.Dependencies) != 1 { - t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) - } - - prodDependencyValues := c.Dependencies()[0].Values - if prodDependencyValues["nameOverride"] != nameOverride { - t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"]) - } -} - -func TestGetAliasDependency(t *testing.T) { - c := loadChart(t, "testdata/frobnitz") - req := c.Metadata.Dependencies - - if len(req) == 0 { - t.Fatalf("there are no dependencies to test") - } - - // Success case - aliasChart := getAliasDependency(c.Dependencies(), req[0]) - if aliasChart == nil { - t.Fatalf("failed to get dependency chart for alias %s", req[0].Name) - } - if req[0].Alias != "" { - if aliasChart.Name() != req[0].Alias { - t.Fatalf("dependency chart name should be %s but got %s", req[0].Alias, aliasChart.Name()) - } - } else if aliasChart.Name() != req[0].Name { - t.Fatalf("dependency chart name should be %s but got %s", req[0].Name, aliasChart.Name()) - } - - if req[0].Version != "" { - if !IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { - t.Fatalf("dependency chart version is not in the compatible range") - } - } - - // Failure case - req[0].Name = "something-else" - if aliasChart := getAliasDependency(c.Dependencies(), req[0]); aliasChart != nil { - t.Fatalf("expected no chart but got %s", aliasChart.Name()) - } - - req[0].Version = "something else which is not in the compatible range" - if IsCompatibleRange(req[0].Version, aliasChart.Metadata.Version) { - t.Fatalf("dependency chart version which is not in the compatible range should cause a failure other than a success ") - } -} - -func TestDependentChartAliases(t *testing.T) { - c := loadChart(t, "testdata/dependent-chart-alias") - req := c.Metadata.Dependencies - - if len(c.Dependencies()) != 2 { - t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) - } - - if err := processDependencyEnabled(c, c.Values, ""); err != nil { - t.Fatalf("expected no errors but got %q", err) - } - - if len(c.Dependencies()) != 3 { - t.Fatal("expected alias dependencies to be added") - } - - if len(c.Dependencies()) != len(c.Metadata.Dependencies) { - t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) - } - - aliasChart := getAliasDependency(c.Dependencies(), req[2]) - - if aliasChart == nil { - t.Fatalf("failed to get dependency chart for alias %s", req[2].Name) - } - if req[2].Alias != "" { - if aliasChart.Name() != req[2].Alias { - t.Fatalf("dependency chart name should be %s but got %s", req[2].Alias, aliasChart.Name()) - } - } else if aliasChart.Name() != req[2].Name { - t.Fatalf("dependency chart name should be %s but got %s", req[2].Name, aliasChart.Name()) - } - - req[2].Name = "dummy-name" - if aliasChart := getAliasDependency(c.Dependencies(), req[2]); aliasChart != nil { - t.Fatalf("expected no chart but got %s", aliasChart.Name()) - } - -} - -func TestDependentChartWithSubChartsAbsentInDependency(t *testing.T) { - c := loadChart(t, "testdata/dependent-chart-no-requirements-yaml") - - if len(c.Dependencies()) != 2 { - t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) - } - - if err := processDependencyEnabled(c, c.Values, ""); err != nil { - t.Fatalf("expected no errors but got %q", err) - } - - if len(c.Dependencies()) != 2 { - t.Fatal("expected no changes in dependencies") - } -} - -func TestDependentChartWithSubChartsHelmignore(t *testing.T) { - // FIXME what does this test? - loadChart(t, "testdata/dependent-chart-helmignore") -} - -func TestDependentChartsWithSubChartsSymlink(t *testing.T) { - joonix := filepath.Join("testdata", "joonix") - if err := os.Symlink(filepath.Join("..", "..", "frobnitz"), filepath.Join(joonix, "charts", "frobnitz")); err != nil { - t.Fatal(err) - } - defer os.RemoveAll(filepath.Join(joonix, "charts", "frobnitz")) - c := loadChart(t, joonix) - - if c.Name() != "joonix" { - t.Fatalf("unexpected chart name: %s", c.Name()) - } - if n := len(c.Dependencies()); n != 1 { - t.Fatalf("expected 1 dependency for this chart, but got %d", n) - } -} - -func TestDependentChartsWithSubchartsAllSpecifiedInDependency(t *testing.T) { - c := loadChart(t, "testdata/dependent-chart-with-all-in-requirements-yaml") - - if len(c.Dependencies()) != 2 { - t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) - } - - if err := processDependencyEnabled(c, c.Values, ""); err != nil { - t.Fatalf("expected no errors but got %q", err) - } - - if len(c.Dependencies()) != 2 { - t.Fatal("expected no changes in dependencies") - } - - if len(c.Dependencies()) != len(c.Metadata.Dependencies) { - t.Fatalf("expected number of chart dependencies %d, but got %d", len(c.Metadata.Dependencies), len(c.Dependencies())) - } -} - -func TestDependentChartsWithSomeSubchartsSpecifiedInDependency(t *testing.T) { - c := loadChart(t, "testdata/dependent-chart-with-mixed-requirements-yaml") - - if len(c.Dependencies()) != 2 { - t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) - } - - if err := processDependencyEnabled(c, c.Values, ""); err != nil { - t.Fatalf("expected no errors but got %q", err) - } - - if len(c.Dependencies()) != 2 { - t.Fatal("expected no changes in dependencies") - } - - if len(c.Metadata.Dependencies) != 1 { - t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) - } -} diff --git a/pkg/helm/pkg/chartutil/doc.go b/pkg/helm/pkg/chartutil/doc.go deleted file mode 100644 index 49c55ac5..00000000 --- a/pkg/helm/pkg/chartutil/doc.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* -Package chartutil contains tools for working with charts. - -Charts are described in the chart package (pkg/chart). -This package provides utilities for serializing and deserializing charts. - -A chart can be represented on the file system in one of two ways: - - - As a directory that contains a Chart.yaml file and other chart things. - - As a tarred gzipped file containing a directory that then contains a - Chart.yaml file. - -This package provides utilities for working with those file formats. - -The preferred way of loading a chart is using 'loader.Load`: - - chart, err := loader.Load(filename) - -This will attempt to discover whether the file at 'filename' is a directory or -a chart archive. It will then load accordingly. - -For accepting raw compressed tar file data from an io.Reader, the -'loader.LoadArchive()' will read in the data, uncompress it, and unpack it -into a Chart. - -When creating charts in memory, use the 'helm.sh/helm/pkg/chart' -package directly. -*/ -package chartutil // import "helm.sh/helm/v3/pkg/chartutil" diff --git a/pkg/helm/pkg/chartutil/errors.go b/pkg/helm/pkg/chartutil/errors.go deleted file mode 100644 index 0a4046d2..00000000 --- a/pkg/helm/pkg/chartutil/errors.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "fmt" -) - -// ErrNoTable indicates that a chart does not have a matching table. -type ErrNoTable struct { - Key string -} - -func (e ErrNoTable) Error() string { return fmt.Sprintf("%q is not a table", e.Key) } - -// ErrNoValue indicates that Values does not contain a key with a value -type ErrNoValue struct { - Key string -} - -func (e ErrNoValue) Error() string { return fmt.Sprintf("%q is not a value", e.Key) } - -type ErrInvalidChartName struct { - Name string -} - -func (e ErrInvalidChartName) Error() string { - return fmt.Sprintf("%q is not a valid chart name", e.Name) -} diff --git a/pkg/helm/pkg/chartutil/expand.go b/pkg/helm/pkg/chartutil/expand.go deleted file mode 100644 index 5dbf6a61..00000000 --- a/pkg/helm/pkg/chartutil/expand.go +++ /dev/null @@ -1,90 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "io" - "os" - "path/filepath" - - securejoin "github.com/cyphar/filepath-securejoin" - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" -) - -// Expand uncompresses and extracts a chart into the specified directory. -func Expand(dir string, r io.Reader) error { - files, err := loader.LoadArchiveFiles(r) - if err != nil { - return err - } - - // Get the name of the chart - var chartName string - for _, file := range files { - if file.Name == "Chart.yaml" { - ch := &chart.Metadata{} - if err := yaml.Unmarshal(file.Data, ch); err != nil { - return errors.Wrap(err, "cannot load Chart.yaml") - } - chartName = ch.Name - } - } - if chartName == "" { - return errors.New("chart name not specified") - } - - // Find the base directory - chartdir, err := securejoin.SecureJoin(dir, chartName) - if err != nil { - return err - } - - // Copy all files verbatim. We don't parse these files because parsing can remove - // comments. - for _, file := range files { - outpath, err := securejoin.SecureJoin(chartdir, file.Name) - if err != nil { - return err - } - - // Make sure the necessary subdirs get created. - basedir := filepath.Dir(outpath) - if err := os.MkdirAll(basedir, 0755); err != nil { - return err - } - - if err := os.WriteFile(outpath, file.Data, 0644); err != nil { - return err - } - } - - return nil -} - -// ExpandFile expands the src file into the dest directory. -func ExpandFile(dest, src string) error { - h, err := os.Open(src) - if err != nil { - return err - } - defer h.Close() - return Expand(dest, h) -} diff --git a/pkg/helm/pkg/chartutil/expand_test.go b/pkg/helm/pkg/chartutil/expand_test.go deleted file mode 100644 index f31a3d29..00000000 --- a/pkg/helm/pkg/chartutil/expand_test.go +++ /dev/null @@ -1,124 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "os" - "path/filepath" - "testing" -) - -func TestExpand(t *testing.T) { - dest := t.TempDir() - - reader, err := os.Open("testdata/frobnitz-1.2.3.tgz") - if err != nil { - t.Fatal(err) - } - - if err := Expand(dest, reader); err != nil { - t.Fatal(err) - } - - expectedChartPath := filepath.Join(dest, "frobnitz") - fi, err := os.Stat(expectedChartPath) - if err != nil { - t.Fatal(err) - } - if !fi.IsDir() { - t.Fatalf("expected a chart directory at %s", expectedChartPath) - } - - dir, err := os.Open(expectedChartPath) - if err != nil { - t.Fatal(err) - } - - fis, err := dir.Readdir(0) - if err != nil { - t.Fatal(err) - } - - expectLen := 11 - if len(fis) != expectLen { - t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) - } - - for _, fi := range fis { - expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) - if err != nil { - t.Fatal(err) - } - // os.Stat can return different values for directories, based on the OS - // for Linux, for example, os.Stat alwaty returns the size of the directory - // (value-4096) regardless of the size of the contents of the directory - mode := expect.Mode() - if !mode.IsDir() { - if fi.Size() != expect.Size() { - t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) - } - } - } -} - -func TestExpandFile(t *testing.T) { - dest := t.TempDir() - - if err := ExpandFile(dest, "testdata/frobnitz-1.2.3.tgz"); err != nil { - t.Fatal(err) - } - - expectedChartPath := filepath.Join(dest, "frobnitz") - fi, err := os.Stat(expectedChartPath) - if err != nil { - t.Fatal(err) - } - if !fi.IsDir() { - t.Fatalf("expected a chart directory at %s", expectedChartPath) - } - - dir, err := os.Open(expectedChartPath) - if err != nil { - t.Fatal(err) - } - - fis, err := dir.Readdir(0) - if err != nil { - t.Fatal(err) - } - - expectLen := 11 - if len(fis) != expectLen { - t.Errorf("Expected %d files, but got %d", expectLen, len(fis)) - } - - for _, fi := range fis { - expect, err := os.Stat(filepath.Join("testdata", "frobnitz", fi.Name())) - if err != nil { - t.Fatal(err) - } - // os.Stat can return different values for directories, based on the OS - // for Linux, for example, os.Stat alwaty returns the size of the directory - // (value-4096) regardless of the size of the contents of the directory - mode := expect.Mode() - if !mode.IsDir() { - if fi.Size() != expect.Size() { - t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) - } - } - } -} diff --git a/pkg/helm/pkg/chartutil/exports.go b/pkg/helm/pkg/chartutil/exports.go deleted file mode 100644 index af77164e..00000000 --- a/pkg/helm/pkg/chartutil/exports.go +++ /dev/null @@ -1,15 +0,0 @@ -package chartutil - -import ( - "log" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -func CoalesceChartValues(c *chart.Chart, v map[string]interface{}, merge bool) { - coalesceValues(log.Printf, c, v, "", merge) -} - -func CoalesceChartDeps(chrt *chart.Chart, dest map[string]interface{}, merge bool) (map[string]interface{}, error) { - return coalesceDeps(log.Printf, chrt, dest, "", merge) -} diff --git a/pkg/helm/pkg/chartutil/jsonschema.go b/pkg/helm/pkg/chartutil/jsonschema.go deleted file mode 100644 index c86afcf6..00000000 --- a/pkg/helm/pkg/chartutil/jsonschema.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "bytes" - "fmt" - "strings" - - "github.com/pkg/errors" - "github.com/xeipuuv/gojsonschema" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -// ValidateAgainstSchema checks that values does not violate the structure laid out in schema -func ValidateAgainstSchema(chrt *chart.Chart, values map[string]interface{}) error { - var sb strings.Builder - if chrt.Schema != nil { - err := ValidateAgainstSingleSchema(values, chrt.Schema) - if err != nil { - sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) - sb.WriteString(err.Error()) - } - } - - // For each dependency, recursively call this function with the coalesced values - for _, subchart := range chrt.Dependencies() { - subchartValues := values[subchart.Name()].(map[string]interface{}) - if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { - sb.WriteString(err.Error()) - } - } - - if sb.Len() > 0 { - return errors.New(sb.String()) - } - - return nil -} - -// ValidateAgainstSingleSchema checks that values does not violate the structure laid out in this schema -func ValidateAgainstSingleSchema(values Values, schemaJSON []byte) (reterr error) { - defer func() { - if r := recover(); r != nil { - reterr = fmt.Errorf("unable to validate schema: %s", r) - } - }() - - valuesData, err := yaml.Marshal(values) - if err != nil { - return err - } - valuesJSON, err := yaml.YAMLToJSON(valuesData) - if err != nil { - return err - } - if bytes.Equal(valuesJSON, []byte("null")) { - valuesJSON = []byte("{}") - } - schemaLoader := gojsonschema.NewBytesLoader(schemaJSON) - valuesLoader := gojsonschema.NewBytesLoader(valuesJSON) - - result, err := gojsonschema.Validate(schemaLoader, valuesLoader) - if err != nil { - return err - } - - if !result.Valid() { - var sb strings.Builder - for _, desc := range result.Errors() { - sb.WriteString(fmt.Sprintf("- %s\n", desc)) - } - return errors.New(sb.String()) - } - - return nil -} diff --git a/pkg/helm/pkg/chartutil/jsonschema_test.go b/pkg/helm/pkg/chartutil/jsonschema_test.go deleted file mode 100644 index 6ecdf396..00000000 --- a/pkg/helm/pkg/chartutil/jsonschema_test.go +++ /dev/null @@ -1,167 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "os" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -func TestValidateAgainstSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - if err := ValidateAgainstSingleSchema(values, schema); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstInvalidSingleSchema(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values-invalid.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := "unable to validate schema: runtime error: invalid " + - "memory address or nil pointer dereference" - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -func TestValidateAgainstSingleSchemaNegative(t *testing.T) { - values, err := ReadValuesFile("./testdata/test-values-negative.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - schema, err := os.ReadFile("./testdata/test-values.schema.json") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - - var errString string - if err := ValidateAgainstSingleSchema(values, schema); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `- (root): employmentInfo is required -- age: Must be greater than or equal to 0 -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} - -const subchartSchema = `{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Values", - "type": "object", - "properties": { - "age": { - "description": "Age", - "minimum": 0, - "type": "integer" - } - }, - "required": [ - "age" - ] -} -` - -func TestValidateAgainstSchema(t *testing.T) { - subchartJSON := []byte(subchartSchema) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{ - "age": 25, - }, - } - - if err := ValidateAgainstSchema(chrt, vals); err != nil { - t.Errorf("Error validating Values against Schema: %s", err) - } -} - -func TestValidateAgainstSchemaNegative(t *testing.T) { - subchartJSON := []byte(subchartSchema) - subchart := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "subchart", - }, - Schema: subchartJSON, - } - chrt := &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "chrt", - }, - } - chrt.AddDependency(subchart) - - vals := map[string]interface{}{ - "name": "John", - "subchart": map[string]interface{}{}, - } - - var errString string - if err := ValidateAgainstSchema(chrt, vals); err == nil { - t.Fatalf("Expected an error, but got nil") - } else { - errString = err.Error() - } - - expectedErrString := `subchart: -- (root): age is required -` - if errString != expectedErrString { - t.Errorf("Error string :\n`%s`\ndoes not match expected\n`%s`", errString, expectedErrString) - } -} diff --git a/pkg/helm/pkg/chartutil/save.go b/pkg/helm/pkg/chartutil/save.go deleted file mode 100644 index 13ea4f28..00000000 --- a/pkg/helm/pkg/chartutil/save.go +++ /dev/null @@ -1,289 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "archive/tar" - "compress/gzip" - "encoding/json" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -var headerBytes = []byte("+aHR0cHM6Ly95b3V0dS5iZS96OVV6MWljandyTQo=") - -func SaveDir(c *chart.Chart, dest string) error { - return SaveIntoDir(c, filepath.Join(dest, c.Name())) -} - -// SaveDir saves a chart as files in a directory. -// -// This takes the chart name, and creates a new subdirectory inside of the given dest -// directory, writing the chart's contents to that subdirectory. -func SaveIntoDir(c *chart.Chart, dest string) error { - // Create the chart directory - err := validateName(c.Name()) - if err != nil { - return err - } - outdir := dest - if fi, err := os.Stat(outdir); err == nil && !fi.IsDir() { - return errors.Errorf("file %s already exists and is not a directory", outdir) - } - if err := os.MkdirAll(outdir, 0755); err != nil { - return err - } - - // Save the chart file. - if err := SaveChartfile(filepath.Join(outdir, ChartfileName), c.Metadata); err != nil { - return err - } - - if c.Metadata.APIVersion == chart.APIVersionV2 { - if c.Lock != nil { - ldata, err := yaml.Marshal(c.Lock) - if err != nil { - return err - } - filename := filepath.Join(outdir, "Chart.lock") - if err := writeFile(filename, ldata); err != nil { - return fmt.Errorf("error writing %q: %s", filename, err) - } - } - } - - // Save values.yaml - for _, f := range c.Raw { - if f.Name == ValuesfileName { - vf := filepath.Join(outdir, ValuesfileName) - if err := writeFile(vf, f.Data); err != nil { - return err - } - } - } - - // Save values.schema.json if it exists - if c.Schema != nil { - filename := filepath.Join(outdir, SchemafileName) - if err := writeFile(filename, c.Schema); err != nil { - return err - } - } - - // Save templates, files, and runtime files (e.g., ts/ for TypeScript charts) - for _, o := range [][]*chart.File{c.Templates, c.Files, c.RuntimeFiles} { - for _, f := range o { - n := filepath.Join(outdir, f.Name) - if err := writeFile(n, f.Data); err != nil { - return err - } - } - } - - // Save dependencies - base := filepath.Join(outdir, ChartsDir) - for _, dep := range c.Dependencies() { - // Here, we write each dependency as a tar file. - if _, err := Save(dep, base); err != nil { - return errors.Wrapf(err, "saving %s", dep.ChartFullPath()) - } - } - return nil -} - -// Save creates an archived chart to the given directory. -// -// This takes an existing chart and a destination directory. -// -// If the directory is /foo, and the chart is named bar, with version 1.0.0, this -// will generate /foo/bar-1.0.0.tgz. -// -// This returns the absolute path to the chart archive file. -func Save(c *chart.Chart, outDir string) (string, error) { - if err := c.Validate(); err != nil { - return "", errors.Wrap(err, "chart validation") - } - - filename := fmt.Sprintf("%s-%s.tgz", c.Name(), c.Metadata.Version) - filename = filepath.Join(outDir, filename) - dir := filepath.Dir(filename) - if stat, err := os.Stat(dir); err != nil { - if os.IsNotExist(err) { - if err2 := os.MkdirAll(dir, 0755); err2 != nil { - return "", err2 - } - } else { - return "", errors.Wrapf(err, "stat %s", dir) - } - } else if !stat.IsDir() { - return "", errors.Errorf("is not a directory: %s", dir) - } - - f, err := os.Create(filename) - if err != nil { - return "", err - } - - // Wrap in gzip writer - zipper := gzip.NewWriter(f) - zipper.Header.Extra = headerBytes - zipper.Header.Comment = "Helm" - - // Wrap in tar writer - twriter := tar.NewWriter(zipper) - rollback := false - defer func() { - twriter.Close() - zipper.Close() - f.Close() - if rollback { - os.Remove(filename) - } - }() - - if err := writeTarContents(twriter, c, ""); err != nil { - rollback = true - return filename, err - } - return filename, nil -} - -func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { - err := validateName(c.Name()) - if err != nil { - return err - } - base := filepath.Join(prefix, c.Name()) - - // Pull out the dependencies of a v1 Chart, since there's no way - // to tell the serializer to skip a field for just this use case - savedDependencies := c.Metadata.Dependencies - if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Metadata.Dependencies = nil - } - // Save Chart.yaml - cdata, err := yaml.Marshal(c.Metadata) - if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Metadata.Dependencies = savedDependencies - } - if err != nil { - return err - } - if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil { - return err - } - - // Save Chart.lock - // TODO: remove the APIVersion check when APIVersionV1 is not used anymore - if c.Metadata.APIVersion == chart.APIVersionV2 { - if c.Lock != nil { - ldata, err := yaml.Marshal(c.Lock) - if err != nil { - return err - } - if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { - return err - } - } - } - - // Save values.yaml - for _, f := range c.Raw { - if f.Name == ValuesfileName { - if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil { - return err - } - } - } - - // Save values.schema.json if it exists - if c.Schema != nil { - if !json.Valid(c.Schema) { - return errors.New("Invalid JSON in " + SchemafileName) - } - if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil { - return err - } - } - - // Save templates - for _, f := range c.Templates { - n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { - return err - } - } - - // Save files - for _, f := range c.Files { - n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { - return err - } - } - - // Save runtime files (e.g., ts/ directory for TypeScript charts) - for _, f := range c.RuntimeFiles { - n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { - return err - } - } - - // Save dependencies - for _, dep := range c.Dependencies() { - if err := writeTarContents(out, dep, filepath.Join(base, ChartsDir)); err != nil { - return err - } - } - return nil -} - -// writeToTar writes a single file to a tar archive. -func writeToTar(out *tar.Writer, name string, body []byte) error { - // TODO: Do we need to create dummy parent directory names if none exist? - h := &tar.Header{ - Name: filepath.ToSlash(name), - Mode: 0644, - Size: int64(len(body)), - ModTime: time.Now(), - } - if err := out.WriteHeader(h); err != nil { - return err - } - _, err := out.Write(body) - return err -} - -// If the name has directory name has characters which would change the location -// they need to be removed. -func validateName(name string) error { - nname := filepath.Base(name) - - if nname != name { - return ErrInvalidChartName{name} - } - - return nil -} diff --git a/pkg/helm/pkg/chartutil/save_test.go b/pkg/helm/pkg/chartutil/save_test.go deleted file mode 100644 index 47098aa3..00000000 --- a/pkg/helm/pkg/chartutil/save_test.go +++ /dev/null @@ -1,264 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "io" - "os" - "path" - "path/filepath" - "regexp" - "strings" - "testing" - "time" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" -) - -func TestSave(t *testing.T) { - tmp := t.TempDir() - - for _, dest := range []string{tmp, filepath.Join(tmp, "newdir")} { - t.Run("outDir="+dest, func(t *testing.T) { - c := &chart.Chart{ - Metadata: &chart.Metadata{ - APIVersion: chart.APIVersionV1, - Name: "ahab", - Version: "1.2.3", - }, - Lock: &chart.Lock{ - Digest: "testdigest", - }, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - Schema: []byte("{\n \"title\": \"Values\"\n}"), - } - chartWithInvalidJSON := withSchema(*c, []byte("{")) - - where, err := Save(c, dest) - if err != nil { - t.Fatalf("Failed to save: %s", err) - } - if !strings.HasPrefix(where, dest) { - t.Fatalf("Expected %q to start with %q", where, dest) - } - if !strings.HasSuffix(where, ".tgz") { - t.Fatalf("Expected %q to end with .tgz", where) - } - - c2, err := loader.LoadFile(where) - if err != nil { - t.Fatal(err) - } - if c2.Name() != c.Name() { - t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) - } - if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { - t.Fatal("Files data did not match") - } - if c2.Lock != nil { - t.Fatal("Expected v1 chart archive not to contain Chart.lock file") - } - - if !bytes.Equal(c.Schema, c2.Schema) { - indentation := 4 - formattedExpected := Indent(indentation, string(c.Schema)) - formattedActual := Indent(indentation, string(c2.Schema)) - t.Fatalf("Schema data did not match.\nExpected:\n%s\nActual:\n%s", formattedExpected, formattedActual) - } - if _, err := Save(&chartWithInvalidJSON, dest); err == nil { - t.Fatalf("Invalid JSON was not caught while saving chart") - } - - c.Metadata.APIVersion = chart.APIVersionV2 - where, err = Save(c, dest) - if err != nil { - t.Fatalf("Failed to save: %s", err) - } - c2, err = loader.LoadFile(where) - if err != nil { - t.Fatal(err) - } - if c2.Lock == nil { - t.Fatal("Expected v2 chart archive to contain a Chart.lock file") - } - if c2.Lock.Digest != c.Lock.Digest { - t.Fatal("Chart.lock data did not match") - } - }) - } - - c := &chart.Chart{ - Metadata: &chart.Metadata{ - APIVersion: chart.APIVersionV1, - Name: "../ahab", - Version: "1.2.3", - }, - Lock: &chart.Lock{ - Digest: "testdigest", - }, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - } - _, err := Save(c, tmp) - if err == nil { - t.Fatal("Expected error saving chart with invalid name") - } -} - -// Creates a copy with a different schema; does not modify anything. -func withSchema(chart chart.Chart, schema []byte) chart.Chart { - chart.Schema = schema - return chart -} - -func Indent(n int, text string) string { - startOfLine := regexp.MustCompile(`(?m)^`) - indentation := strings.Repeat(" ", n) - return startOfLine.ReplaceAllLiteralString(text, indentation) -} - -func TestSavePreservesTimestamps(t *testing.T) { - // Test executes so quickly that if we don't subtract a second, the - // check will fail because `initialCreateTime` will be identical to the - // written timestamp for the files. - initialCreateTime := time.Now().Add(-1 * time.Second) - - tmp := t.TempDir() - - c := &chart.Chart{ - Metadata: &chart.Metadata{ - APIVersion: chart.APIVersionV1, - Name: "ahab", - Version: "1.2.3", - }, - Values: map[string]interface{}{ - "imageName": "testimage", - "imageId": 42, - }, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - Schema: []byte("{\n \"title\": \"Values\"\n}"), - } - - where, err := Save(c, tmp) - if err != nil { - t.Fatalf("Failed to save: %s", err) - } - - allHeaders, err := retrieveAllHeadersFromTar(where) - if err != nil { - t.Fatalf("Failed to parse tar: %v", err) - } - - for _, header := range allHeaders { - if header.ModTime.Before(initialCreateTime) { - t.Fatalf("File timestamp not preserved: %v", header.ModTime) - } - } -} - -// We could refactor `load.go` to use this `retrieveAllHeadersFromTar` function -// as well, so we are not duplicating components of the code which iterate -// through the tar. -func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { - raw, err := os.Open(path) - if err != nil { - return nil, err - } - defer raw.Close() - - unzipped, err := gzip.NewReader(raw) - if err != nil { - return nil, err - } - defer unzipped.Close() - - tr := tar.NewReader(unzipped) - headers := []*tar.Header{} - for { - hd, err := tr.Next() - if err == io.EOF { - break - } - - if err != nil { - return nil, err - } - - headers = append(headers, hd) - } - - return headers, nil -} - -func TestSaveDir(t *testing.T) { - tmp := t.TempDir() - - c := &chart.Chart{ - Metadata: &chart.Metadata{ - APIVersion: chart.APIVersionV1, - Name: "ahab", - Version: "1.2.3", - }, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - Templates: []*chart.File{ - {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, - }, - } - - if err := SaveDir(c, tmp); err != nil { - t.Fatalf("Failed to save: %s", err) - } - - c2, err := loader.LoadDir(tmp + "/ahab") - if err != nil { - t.Fatal(err) - } - - if c2.Name() != c.Name() { - t.Fatalf("Expected chart archive to have %q, got %q", c.Name(), c2.Name()) - } - - if len(c2.Templates) != 1 || c2.Templates[0].Name != c.Templates[0].Name { - t.Fatal("Templates data did not match") - } - - if len(c2.Files) != 1 || c2.Files[0].Name != c.Files[0].Name { - t.Fatal("Files data did not match") - } - - tmp2 := t.TempDir() - c.Metadata.Name = "../ahab" - pth := filepath.Join(tmp2, "tmpcharts") - if err := os.MkdirAll(filepath.Join(pth), 0755); err != nil { - t.Fatal(err) - } - - if err := SaveDir(c, pth); err.Error() != "\"../ahab\" is not a valid chart name" { - t.Fatalf("Did not get expected error for chart named %q", c.Name()) - } -} diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/Chart.yaml b/pkg/helm/pkg/chartutil/testdata/subpop/Chart.yaml deleted file mode 100644 index e4cf05b7..00000000 --- a/pkg/helm/pkg/chartutil/testdata/subpop/Chart.yaml +++ /dev/null @@ -1,51 +0,0 @@ -apiVersion: v1 -description: A Helm chart for Kubernetes -name: parentchart -version: 0.1.0 -dependencies: - - name: subchart1 - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart1.enabled - tags: - - front-end - - subchart1 - import-values: - - child: SC1data - parent: imported-chart1 - - child: SC1data - parent: overridden-chart1 - - child: imported-chartA - parent: imported-chartA - - child: imported-chartA-B - parent: imported-chartA-B - - child: overridden-chartA-B - parent: overridden-chartA-B - - child: SCBexported1A - parent: . - - SCBexported2 - - SC1exported1 - export-values: - - parent: exported-parent - child: exported-parent - - parent: exported-overridden-parent - child: exported-overridden-parent - - parent: exported-single-value-parent - child: exported-single-value-parent - - parent: exported-passthrough - child: subcharta.exported-passthrough - - exported-short-parent - - - name: subchart2 - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart2.enabled - tags: - - back-end - - subchart2 - - - name: subchart2 - alias: subchart2alias - repository: http://localhost:10191 - version: 0.1.0 - condition: subchart2alias.enabled diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml b/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml deleted file mode 100644 index a0be53c4..00000000 --- a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/Chart.yaml +++ /dev/null @@ -1,40 +0,0 @@ -apiVersion: v1 -description: A Helm chart for Kubernetes -name: subchart1 -version: 0.1.0 -dependencies: - - name: subcharta - repository: http://localhost:10191 - version: 0.1.0 - condition: subcharta.enabled - tags: - - front-end - - subcharta - import-values: - - child: SCAdata - parent: imported-chartA - - child: SCAdata - parent: overridden-chartA - - child: SCAdata - parent: imported-chartA-B - export-values: - - parent: exported-overridden-chart1 - child: exported-overridden-chart1 - - exported-short-chart1 - - - name: subchartb - repository: http://localhost:10191 - version: 0.1.0 - condition: subchartb.enabled - import-values: - - child: SCBdata - parent: imported-chartB - - child: SCBdata - parent: imported-chartA-B - - child: exports.SCBexported2 - parent: exports.SCBexported2 - - SCBexported1 - - tags: - - front-end - - subchartb diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml b/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml deleted file mode 100644 index 0ea6cc5c..00000000 --- a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml +++ /dev/null @@ -1,29 +0,0 @@ -# Default values for subchart. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. -# subchartA -service: - name: apache - type: ClusterIP - externalPort: 80 - internalPort: 80 -SCAdata: - SCAbool: false - SCAfloat: 3.1 - SCAint: 55 - SCAstring: "jabba" - SCAnested1: - SCAnested2: true - -exported-overridden-chart1: - SCAbool: true - SCAfloat: 33.3 - SCAint: 333 - SCAstring: "exported-from-chart1" - SPExtra15: "should-be-unchanged" - -exported-passthrough: - SPExtra18: "should-be-unchanged" - -exported-short-chart1: - SPExtra16: "should-be-unchanged" \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml b/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml deleted file mode 100644 index 19d4fd70..00000000 --- a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/values.yaml +++ /dev/null @@ -1,78 +0,0 @@ -# Default values for subchart. -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. -# subchart1 -service: - name: nginx - type: ClusterIP - externalPort: 80 - internalPort: 80 - - -SC1data: - SC1bool: true - SC1float: 3.14 - SC1int: 100 - SC1string: "dollywood" - SC1extra1: 11 - -imported-chartA: - SC1extra2: 1.337 - -overridden-chartA: - SCAbool: true - SCAfloat: 3.14 - SCAint: 100 - SCAstring: "jabbathehut" - SC1extra3: true - -imported-chartA-B: - SC1extra5: "tiller" - -exported-parent: - SPExtra10: "should-be-unchanged" - -exported-overridden-parent: - SC1bool: false - SC1float: 11.1 - SC1int: 111 - SC1string: "should-be-overridden" - SPExtra11: "should-be-unchanged" - -exported-short-parent: - SPExtra12: "should-be-unchanged" - -exported-overridden-chart1: - SCAbool: true - SCAfloat: 33.3 - SCAint: 333 - SCAstring: "exported-from-chart1" - SPExtra13: "exported-from-chart1-n2" - -overridden-chartA-B: - SCAbool: true - SCAfloat: 3.33 - SCAint: 555 - SCAstring: "wormwood" - SCAextra1: 23 - - SCBbool: true - SCBfloat: 0.25 - SCBint: 98 - SCBstring: "murkwood" - SCBextra1: 13 - - SC1extra6: 77 - -SCBexported1A: - SC1extra7: true - -exports: - SC1exported1: - global: - SC1exported2: - all: - SC1exported3: "SC1expstr" - exported-short-chart1: - exported-short-chart1: - SPExtra14: "exported-from-chart1-n3" \ No newline at end of file diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml b/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml deleted file mode 100644 index 3f168bdb..00000000 --- a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: subchart2-{{ .Chart.Name }} - labels: - helm.sh/hart: "{{ .Chart.Name }}-{{ .Chart.Version }}" -spec: - type: {{ .Values.service.type }} - ports: - - port: {{ .Values.service.externalPort }} - targetPort: {{ .Values.service.internalPort }} - protocol: TCP - name: subchart2-{{ .Values.service.name }} - selector: - app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/values.yaml b/pkg/helm/pkg/chartutil/testdata/subpop/values.yaml deleted file mode 100644 index 4588ef0e..00000000 --- a/pkg/helm/pkg/chartutil/testdata/subpop/values.yaml +++ /dev/null @@ -1,67 +0,0 @@ -# parent/values.yaml - -imported-chart1: - SPextra1: "helm rocks" - -overridden-chart1: - SC1bool: false - SC1float: 3.141592 - SC1int: 99 - SC1string: "pollywog" - SPextra2: 42 - - -imported-chartA: - SPextra3: 1.337 - -overridden-chartA: - SCAbool: true - SCAfloat: 41.3 - SCAint: 808 - SCAstring: "jabberwocky" - SPextra4: true - -imported-chartA-B: - SPextra5: "k8s" - -overridden-chartA-B: - SCAbool: true - SCAfloat: 41.3 - SCAint: 808 - SCAstring: "jabberwocky" - SCBbool: false - SCBfloat: 1.99 - SCBint: 77 - SCBstring: "jango" - SPextra6: 111 - -exported-parent: - SPExtra7: "exported-from-parent" - SPNested1: - SPExtra19: "exported-from-parent-n6" - -exported-overridden-parent: - SC1bool: true - SC1float: 22.2 - SC1int: 222 - SC1string: "exported-from-parent-n2" - SPExtra8: "exported-from-parent-n3" - -exported-single-value-parent: "exported-from-parent-n7" - -exported-passthrough: - SPExtra17: "exported-from-parent-n5" - -exports: - exported-short-parent: - exported-short-parent: - SPExtra9: "exported-from-parent-n4" - -tags: - front-end: true - back-end: false - -subchart2alias: - enabled: false - -ensurenull: null diff --git a/pkg/helm/pkg/chartutil/validate_name.go b/pkg/helm/pkg/chartutil/validate_name.go deleted file mode 100644 index 05c090cb..00000000 --- a/pkg/helm/pkg/chartutil/validate_name.go +++ /dev/null @@ -1,112 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "fmt" - "regexp" - - "github.com/pkg/errors" -) - -// validName is a regular expression for resource names. -// -// According to the Kubernetes help text, the regular expression it uses is: -// -// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* -// -// This follows the above regular expression (but requires a full string match, not partial). -// -// The Kubernetes documentation is here, though it is not entirely correct: -// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names -var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) - -var ( - // errMissingName indicates that a release (name) was not provided. - errMissingName = errors.New("no name provided") - - // errInvalidName indicates that an invalid release name was provided - errInvalidName = fmt.Errorf( - "invalid release name, must match regex %s and the length must not be longer than 53", - validName.String()) - - // errInvalidKubernetesName indicates that the name does not meet the Kubernetes - // restrictions on metadata names. - errInvalidKubernetesName = fmt.Errorf( - "invalid metadata name, must match regex %s and the length must not be longer than 253", - validName.String()) -) - -const ( - // According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names) - // some resource names have a max length of 63 characters while others have a max - // length of 253 characters. As we cannot be sure the resources used in a chart, we - // therefore need to limit it to 63 chars and reserve 10 chars for additional part to name - // of the resource. The reason is that chart maintainers can use release name as part of - // the resource name (and some additional chars). - maxReleaseNameLen = 53 - // maxMetadataNameLen is the maximum length Kubernetes allows for any name. - maxMetadataNameLen = 253 -) - -// ValidateReleaseName performs checks for an entry for a Helm release name -// -// For Helm to allow a name, it must be below a certain character count (53) and also match -// a regular expression. -// -// According to the Kubernetes help text, the regular expression it uses is: -// -// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* -// -// This follows the above regular expression (but requires a full string match, not partial). -// -// The Kubernetes documentation is here, though it is not entirely correct: -// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names -func ValidateReleaseName(name string) error { - // This case is preserved for backwards compatibility - if name == "" { - return errMissingName - - } - if len(name) > maxReleaseNameLen || !validName.MatchString(name) { - return errInvalidName - } - return nil -} - -// ValidateMetadataName validates the name field of a Kubernetes metadata object. -// -// Empty strings, strings longer than 253 chars, or strings that don't match the regexp -// will fail. -// -// According to the Kubernetes help text, the regular expression it uses is: -// -// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* -// -// This follows the above regular expression (but requires a full string match, not partial). -// -// The Kubernetes documentation is here, though it is not entirely correct: -// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names -// -// Deprecated: remove in Helm 4. Name validation now uses rules defined in -// pkg/lint/rules.validateMetadataNameFunc() -func ValidateMetadataName(name string) error { - if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) { - return errInvalidKubernetesName - } - return nil -} diff --git a/pkg/helm/pkg/chartutil/validate_name_test.go b/pkg/helm/pkg/chartutil/validate_name_test.go deleted file mode 100644 index 5f0792f9..00000000 --- a/pkg/helm/pkg/chartutil/validate_name_test.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import "testing" - -// TestValidateName is a regression test for ValidateName -// -// Kubernetes has strict naming conventions for resource names. This test represents -// those conventions. -// -// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names -// -// NOTE: At the time of this writing, the docs above say that names cannot begin with -// digits. However, `kubectl`'s regular expression explicit allows this, and -// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits. -func TestValidateReleaseName(t *testing.T) { - names := map[string]bool{ - "": false, - "foo": true, - "foo.bar1234baz.seventyone": true, - "FOO": false, - "123baz": true, - "foo.BAR.baz": false, - "one-two": true, - "-two": false, - "one_two": false, - "a..b": false, - "%^&#$%*@^*@&#^": false, - "example:com": false, - "example%%com": false, - "a1111111111111111111111111111111111111111111111111111111111z": false, - } - for input, expectPass := range names { - if err := ValidateReleaseName(input); (err == nil) != expectPass { - st := "fail" - if expectPass { - st = "succeed" - } - t.Errorf("Expected %q to %s", input, st) - } - } -} - -func TestValidateMetadataName(t *testing.T) { - names := map[string]bool{ - "": false, - "foo": true, - "foo.bar1234baz.seventyone": true, - "FOO": false, - "123baz": true, - "foo.BAR.baz": false, - "one-two": true, - "-two": false, - "one_two": false, - "a..b": false, - "%^&#$%*@^*@&#^": false, - "example:com": false, - "example%%com": false, - "a1111111111111111111111111111111111111111111111111111111111z": true, - "a1111111111111111111111111111111111111111111111111111111111z" + - "a1111111111111111111111111111111111111111111111111111111111z" + - "a1111111111111111111111111111111111111111111111111111111111z" + - "a1111111111111111111111111111111111111111111111111111111111z" + - "a1111111111111111111111111111111111111111111111111111111111z" + - "a1111111111111111111111111111111111111111111111111111111111z": false, - } - for input, expectPass := range names { - if err := ValidateMetadataName(input); (err == nil) != expectPass { - st := "fail" - if expectPass { - st = "succeed" - } - t.Errorf("Expected %q to %s", input, st) - } - } -} diff --git a/pkg/helm/pkg/chartutil/values.go b/pkg/helm/pkg/chartutil/values.go deleted file mode 100644 index 22ba586e..00000000 --- a/pkg/helm/pkg/chartutil/values.go +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "fmt" - "io" - "log" - "os" - "strings" - - "github.com/pkg/errors" - "github.com/samber/lo" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -// GlobalKey is the name of the Values key that is used for storing global vars. -const GlobalKey = "global" - -// Values represents a collection of chart values. -type Values map[string]interface{} - -// YAML encodes the Values into a YAML string. -func (v Values) YAML() (string, error) { - b, err := yaml.Marshal(v) - return string(b), err -} - -// Table gets a table (YAML subsection) from a Values object. -// -// The table is returned as a Values. -// -// Compound table names may be specified with dots: -// -// foo.bar -// -// The above will be evaluated as "The table bar inside the table -// foo". -// -// An ErrNoTable is returned if the table does not exist. -func (v Values) Table(name string) (Values, error) { - table := v - var err error - - for _, n := range parsePath(name) { - if table, err = tableLookup(table, n); err != nil { - break - } - } - return table, err -} - -// AsMap is a utility function for converting Values to a map[string]interface{}. -// -// It protects against nil map panics. -func (v Values) AsMap() map[string]interface{} { - if len(v) == 0 { - return map[string]interface{}{} - } - return v -} - -// Encode writes serialized Values information to the given io.Writer. -func (v Values) Encode(w io.Writer) error { - out, err := yaml.Marshal(v) - if err != nil { - return err - } - _, err = w.Write(out) - return err -} - -func tableLookup(v Values, simple string) (Values, error) { - v2, ok := v[simple] - if !ok { - return v, ErrNoTable{simple} - } - if vv, ok := v2.(map[string]interface{}); ok { - return vv, nil - } - - // This catches a case where a value is of type Values, but doesn't (for some - // reason) match the map[string]interface{}. This has been observed in the - // wild, and might be a result of a nil map of type Values. - if vv, ok := v2.(Values); ok { - return vv, nil - } - - return Values{}, ErrNoTable{simple} -} - -// ReadValues will parse YAML byte data into a Values. -func ReadValues(data []byte) (vals Values, err error) { - err = yaml.Unmarshal(data, &vals) - if len(vals) == 0 { - vals = Values{} - } - return vals, err -} - -// ReadValuesFile will parse a YAML file into a map of values. -func ReadValuesFile(filename string) (Values, error) { - data, err := os.ReadFile(filename) - if err != nil { - return map[string]interface{}{}, err - } - return ReadValues(data) -} - -// ReleaseOptions represents the additional release options needed -// for the composition of the final values struct -type ReleaseOptions struct { - Name string - Namespace string - Revision int - IsUpgrade bool - IsInstall bool -} - -// ToRenderValues composes the struct from the data coming from the Releases, Charts and Values files -// -// This takes both ReleaseOptions and Capabilities to merge into the render values. -func ToRenderValues(chrt *chart.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, runtime, defaultRootContext map[string]interface{}) (Values, error) { - if caps == nil { - caps = DefaultCapabilities - } - - top := map[string]interface{}{ - "Chart": chrt.Metadata, - "Capabilities": caps, - "Release": map[string]interface{}{ - "Name": options.Name, - "Namespace": options.Namespace, - "IsUpgrade": options.IsUpgrade, - "IsInstall": options.IsInstall, - "Revision": options.Revision, - "Service": "Helm", - }, - "Runtime": runtime, - } - - top = lo.Assign(defaultRootContext, top) - - vals, err := CoalesceValues(chrt, chrtVals) - if err != nil { - return top, err - } - - if err := ValidateAgainstSchema(chrt, vals); err != nil { - errFmt := "values don't meet the specifications of the schema(s) in the following chart(s):\n%s" - - if strings.Contains(err.Error(), "(root): Additional property werf is not allowed") { - log.Printf("Warning: %s", fmt.Sprintf(errFmt, err.Error())) - } else { - return top, fmt.Errorf(errFmt, err.Error()) - } - } - - top["Values"] = vals - return top, nil -} - -// istable is a special-purpose function to see if the present thing matches the definition of a YAML table. -func istable(v interface{}) bool { - _, ok := v.(map[string]interface{}) - return ok -} - -// PathValue takes a path that traverses a YAML structure and returns the value at the end of that path. -// The path starts at the root of the YAML structure and is comprised of YAML keys separated by periods. -// Given the following YAML data the value at path "chapter.one.title" is "Loomings". -// -// chapter: -// one: -// title: "Loomings" -func (v Values) PathValue(path string) (interface{}, error) { - if path == "" { - return nil, errors.New("YAML path cannot be empty") - } - return v.pathValue(parsePath(path)) -} - -func (v Values) pathValue(path []string) (interface{}, error) { - if len(path) == 1 { - // if exists must be root key not table - if _, ok := v[path[0]]; ok && !istable(v[path[0]]) { - return v[path[0]], nil - } - return nil, ErrNoValue{path[0]} - } - - key, path := path[len(path)-1], path[:len(path)-1] - // get our table for table path - t, err := v.Table(joinPath(path...)) - if err != nil { - return nil, ErrNoValue{key} - } - // check table for key and ensure value is not a table - if k, ok := t[key]; ok && !istable(k) { - return k, nil - } - return nil, ErrNoValue{key} -} - -func parsePath(key string) []string { return strings.Split(key, ".") } - -func joinPath(path ...string) string { return strings.Join(path, ".") } diff --git a/pkg/helm/pkg/chartutil/values_test.go b/pkg/helm/pkg/chartutil/values_test.go deleted file mode 100644 index 66c50cb5..00000000 --- a/pkg/helm/pkg/chartutil/values_test.go +++ /dev/null @@ -1,292 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package chartutil - -import ( - "bytes" - "fmt" - "testing" - "text/template" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -func TestReadValues(t *testing.T) { - doc := `# Test YAML parse -poet: "Coleridge" -title: "Rime of the Ancient Mariner" -stanza: - - "at" - - "length" - - "did" - - cross - - an - - Albatross - -mariner: - with: "crossbow" - shot: "ALBATROSS" - -water: - water: - where: "everywhere" - nor: "any drop to drink" -` - - data, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Error parsing bytes: %s", err) - } - matchValues(t, data) - - tests := []string{`poet: "Coleridge"`, "# Just a comment", ""} - - for _, tt := range tests { - data, err = ReadValues([]byte(tt)) - if err != nil { - t.Fatalf("Error parsing bytes (%s): %s", tt, err) - } - if data == nil { - t.Errorf(`YAML string "%s" gave a nil map`, tt) - } - } -} - -func TestToRenderValues(t *testing.T) { - - chartValues := map[string]interface{}{ - "name": "al Rashid", - "where": map[string]interface{}{ - "city": "Basrah", - "title": "caliph", - }, - } - - overrideValues := map[string]interface{}{ - "name": "Haroun", - "where": map[string]interface{}{ - "city": "Baghdad", - "date": "809 CE", - }, - } - - c := &chart.Chart{ - Metadata: &chart.Metadata{Name: "test"}, - Templates: []*chart.File{}, - Values: chartValues, - Files: []*chart.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, - }, - } - c.AddDependency(&chart.Chart{ - Metadata: &chart.Metadata{Name: "where"}, - }) - - o := ReleaseOptions{ - Name: "Seven Voyages", - Namespace: "default", - Revision: 1, - IsInstall: true, - } - - res, err := ToRenderValues(c, overrideValues, o, nil, nil) - if err != nil { - t.Fatal(err) - } - - // Ensure that the top-level values are all set. - if name := res["Chart"].(*chart.Metadata).Name; name != "test" { - t.Errorf("Expected chart name 'test', got %q", name) - } - relmap := res["Release"].(map[string]interface{}) - if name := relmap["Name"]; name.(string) != "Seven Voyages" { - t.Errorf("Expected release name 'Seven Voyages', got %q", name) - } - if namespace := relmap["Namespace"]; namespace.(string) != "default" { - t.Errorf("Expected namespace 'default', got %q", namespace) - } - if revision := relmap["Revision"]; revision.(int) != 1 { - t.Errorf("Expected revision '1', got %d", revision) - } - if relmap["IsUpgrade"].(bool) { - t.Error("Expected upgrade to be false.") - } - if !relmap["IsInstall"].(bool) { - t.Errorf("Expected install to be true.") - } - if !res["Capabilities"].(*Capabilities).APIVersions.Has("v1") { - t.Error("Expected Capabilities to have v1 as an API") - } - if res["Capabilities"].(*Capabilities).KubeVersion.Major != "1" { - t.Error("Expected Capabilities to have a Kube version") - } - - vals := res["Values"].(Values) - if vals["name"] != "Haroun" { - t.Errorf("Expected 'Haroun', got %q (%v)", vals["name"], vals) - } - where := vals["where"].(map[string]interface{}) - expects := map[string]string{ - "city": "Baghdad", - "date": "809 CE", - "title": "caliph", - } - for field, expect := range expects { - if got := where[field]; got != expect { - t.Errorf("Expected %q, got %q (%v)", expect, got, where) - } - } -} - -func TestReadValuesFile(t *testing.T) { - data, err := ReadValuesFile("./testdata/coleridge.yaml") - if err != nil { - t.Fatalf("Error reading YAML file: %s", err) - } - matchValues(t, data) -} - -func ExampleValues() { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - panic(err) - } - ch1, err := d.Table("chapter.one") - if err != nil { - panic("could not find chapter one") - } - fmt.Print(ch1["title"]) - // Output: - // Loomings -} - -func TestTable(t *testing.T) { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Failed to parse the White Whale: %s", err) - } - - if _, err := d.Table("title"); err == nil { - t.Fatalf("Title is not a table.") - } - - if _, err := d.Table("chapter"); err != nil { - t.Fatalf("Failed to get the chapter table: %s\n%v", err, d) - } - - if v, err := d.Table("chapter.one"); err != nil { - t.Errorf("Failed to get chapter.one: %s", err) - } else if v["title"] != "Loomings" { - t.Errorf("Unexpected title: %s", v["title"]) - } - - if _, err := d.Table("chapter.three"); err != nil { - t.Errorf("Chapter three is missing: %s\n%v", err, d) - } - - if _, err := d.Table("chapter.OneHundredThirtySix"); err == nil { - t.Errorf("I think you mean 'Epilogue'") - } -} - -func matchValues(t *testing.T, data map[string]interface{}) { - if data["poet"] != "Coleridge" { - t.Errorf("Unexpected poet: %s", data["poet"]) - } - - if o, err := ttpl("{{len .stanza}}", data); err != nil { - t.Errorf("len stanza: %s", err) - } else if o != "6" { - t.Errorf("Expected 6, got %s", o) - } - - if o, err := ttpl("{{.mariner.shot}}", data); err != nil { - t.Errorf(".mariner.shot: %s", err) - } else if o != "ALBATROSS" { - t.Errorf("Expected that mariner shot ALBATROSS") - } - - if o, err := ttpl("{{.water.water.where}}", data); err != nil { - t.Errorf(".water.water.where: %s", err) - } else if o != "everywhere" { - t.Errorf("Expected water water everywhere") - } -} - -func ttpl(tpl string, v map[string]interface{}) (string, error) { - var b bytes.Buffer - tt := template.Must(template.New("t").Parse(tpl)) - err := tt.Execute(&b, v) - return b.String(), err -} - -func TestPathValue(t *testing.T) { - doc := ` -title: "Moby Dick" -chapter: - one: - title: "Loomings" - two: - title: "The Carpet-Bag" - three: - title: "The Spouter Inn" -` - d, err := ReadValues([]byte(doc)) - if err != nil { - t.Fatalf("Failed to parse the White Whale: %s", err) - } - - if v, err := d.PathValue("chapter.one.title"); err != nil { - t.Errorf("Got error instead of title: %s\n%v", err, d) - } else if v != "Loomings" { - t.Errorf("No error but got wrong value for title: %s\n%v", err, d) - } - if _, err := d.PathValue("chapter.one.doesnotexist"); err == nil { - t.Errorf("Non-existent key should return error: %s\n%v", err, d) - } - if _, err := d.PathValue("chapter.doesnotexist.one"); err == nil { - t.Errorf("Non-existent key in middle of path should return error: %s\n%v", err, d) - } - if _, err := d.PathValue(""); err == nil { - t.Error("Asking for the value from an empty path should yield an error") - } - if v, err := d.PathValue("title"); err == nil { - if v != "Moby Dick" { - t.Errorf("Failed to return values for root key title") - } - } -} diff --git a/pkg/helm/pkg/cli/environment.go b/pkg/helm/pkg/cli/environment.go index aa6a24c8..679abfed 100644 --- a/pkg/helm/pkg/cli/environment.go +++ b/pkg/helm/pkg/cli/environment.go @@ -36,6 +36,7 @@ import ( "github.com/werf/nelm/pkg/helm/intern/version" "github.com/werf/nelm/pkg/helm/pkg/helmpath" + "github.com/werf/nelm/pkg/helm/pkg/kube" ) // defaultMaxHistory sets the maximum number of releases to 0: unlimited @@ -44,13 +45,13 @@ const defaultMaxHistory = 10 // defaultBurstLimit sets the default client-side throttling limit const defaultBurstLimit = 100 -// defaultQPS sets the default QPS value to 0 to to use library defaults unless specified +// defaultQPS sets the default QPS value to 0 to use library defaults unless specified const defaultQPS = float32(0) // EnvSettings describes all of the environment settings. type EnvSettings struct { namespace string - config genericclioptions.RESTClientGetter + config *genericclioptions.ConfigFlags // KubeConfig is the path to the kubeconfig file KubeConfig string @@ -88,12 +89,17 @@ type EnvSettings struct { BurstLimit int // QPS is queries per second which may be used to avoid throttling. QPS float32 + // ColorMode controls colorized output (never, auto, always) + ColorMode string + // ContentCache is the location where cached charts are stored + ContentCache string } func New() *EnvSettings { env := &EnvSettings{ namespace: os.Getenv("HELM_NAMESPACE"), MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory), + KubeConfig: os.Getenv("KUBECONFIG"), KubeContext: os.Getenv("HELM_KUBECONTEXT"), KubeToken: os.Getenv("HELM_KUBETOKEN"), KubeAsUser: os.Getenv("HELM_KUBEASUSER"), @@ -106,13 +112,15 @@ func New() *EnvSettings { RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry/config.json")), RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), + ContentCache: envOr("HELM_CONTENT_CACHE", helmpath.CachePath("content")), BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), QPS: envFloat32Or("HELM_QPS", defaultQPS), + ColorMode: envColorMode(), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) // bind to kubernetes config flags - env.config = &genericclioptions.ConfigFlags{ + config := &genericclioptions.ConfigFlags{ Namespace: &env.namespace, Context: &env.KubeContext, BearerToken: &env.KubeToken, @@ -127,12 +135,17 @@ func New() *EnvSettings { config.Burst = env.BurstLimit config.QPS = env.QPS config.Wrap(func(rt http.RoundTripper) http.RoundTripper { - return &retryingRoundTripper{wrapped: rt} + return &kube.RetryingRoundTripper{Wrapped: rt} }) config.UserAgent = version.GetUserAgent() return config }, } + if env.BurstLimit != defaultBurstLimit { + config = config.WithDiscoveryBurst(env.BurstLimit) + } + env.config = config + return env } @@ -151,9 +164,12 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output") fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file") fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs") - fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes") + fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the directory containing cached repository indexes") + fs.StringVar(&s.ContentCache, "content-cache", s.ContentCache, "path to the directory containing cached content (e.g. charts)") fs.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "client-side default throttling limit") fs.Float32Var(&s.QPS, "qps", s.QPS, "queries per second used when communicating with the Kubernetes API, not including bursting") + fs.StringVar(&s.ColorMode, "color", s.ColorMode, "use colored output (never, auto, always)") + fs.StringVar(&s.ColorMode, "colour", s.ColorMode, "use colored output (never, auto, always)") } func envOr(name, def string) string { @@ -207,6 +223,23 @@ func envCSV(name string) (ls []string) { return } +func envColorMode() string { + // Check NO_COLOR environment variable first (standard) + if v, ok := os.LookupEnv("NO_COLOR"); ok && v != "" { + return "never" + } + // Check HELM_COLOR environment variable + if v, ok := os.LookupEnv("HELM_COLOR"); ok { + v = strings.ToLower(v) + switch v { + case "never", "auto", "always": + return v + } + } + // Default to auto + return "auto" +} + func (s *EnvSettings) EnvVars() map[string]string { envvars := map[string]string{ "HELM_BIN": os.Args[0], @@ -217,6 +250,7 @@ func (s *EnvSettings) EnvVars() map[string]string { "HELM_PLUGINS": s.PluginsDirectory, "HELM_REGISTRY_CONFIG": s.RegistryConfig, "HELM_REPOSITORY_CACHE": s.RepositoryCache, + "HELM_CONTENT_CACHE": s.ContentCache, "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, "HELM_NAMESPACE": s.Namespace(), "HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory), @@ -241,13 +275,14 @@ func (s *EnvSettings) EnvVars() map[string]string { // Namespace gets the namespace from the configuration func (s *EnvSettings) Namespace() string { + if s.config != nil { + if ns, _, err := s.config.ToRawKubeConfigLoader().Namespace(); err == nil { + return ns + } + } if s.namespace != "" { return s.namespace } - - if ns, _, err := s.config.ToRawKubeConfigLoader().Namespace(); err == nil { - return ns - } return "default" } @@ -260,3 +295,9 @@ func (s *EnvSettings) SetNamespace(namespace string) { func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter { return s.config } + +// ShouldDisableColor returns true if color output should be disabled. +// Color is only enabled when ColorMode is explicitly set to "always". +func (s *EnvSettings) ShouldDisableColor() bool { + return s.ColorMode != "always" +} diff --git a/pkg/helm/pkg/cli/environment_test.go b/pkg/helm/pkg/cli/environment_test.go index 329f320c..2ebd6fbe 100644 --- a/pkg/helm/pkg/cli/environment_test.go +++ b/pkg/helm/pkg/cli/environment_test.go @@ -38,7 +38,6 @@ func TestSetNamespace(t *testing.T) { if settings.namespace != "testns" { t.Errorf("Expected namespace testns, got %s", settings.namespace) } - } func TestEnvSettings(t *testing.T) { @@ -111,6 +110,14 @@ func TestEnvSettings(t *testing.T) { kubeTLSServer: "example.org", kubeInsecure: true, }, + { + name: "invalid kubeconfig", + ns: "testns", + args: "--namespace=testns --kubeconfig=/path/to/fake/file", + maxhistory: defaultMaxHistory, + burstLimit: defaultBurstLimit, + qps: defaultQPS, + }, } for _, tt := range tests { @@ -118,7 +125,7 @@ func TestEnvSettings(t *testing.T) { defer resetEnv()() for k, v := range tt.envvars { - os.Setenv(k, v) + t.Setenv(k, v) } flags := pflag.NewFlagSet("testing", pflag.ContinueOnError) @@ -225,10 +232,7 @@ func TestEnvOrBool(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.env != "" { - t.Cleanup(func() { - os.Unsetenv(tt.env) - }) - os.Setenv(tt.env, tt.val) + t.Setenv(tt.env, tt.val) } actual := envBoolOr(tt.env, tt.def) if actual != tt.expected { diff --git a/pkg/helm/pkg/cli/exports.go b/pkg/helm/pkg/cli/exports.go deleted file mode 100644 index 7de52a37..00000000 --- a/pkg/helm/pkg/cli/exports.go +++ /dev/null @@ -1,13 +0,0 @@ -package cli - -import "k8s.io/cli-runtime/pkg/genericclioptions" - -var EnvOr = envOr - -func (s *EnvSettings) GetNamespaceP() *string { - return &s.namespace -} - -func (s *EnvSettings) GetConfigP() *genericclioptions.RESTClientGetter { - return &s.config -} diff --git a/pkg/helm/pkg/cli/output/output.go b/pkg/helm/pkg/cli/output/output.go index a46c977a..28d50374 100644 --- a/pkg/helm/pkg/cli/output/output.go +++ b/pkg/helm/pkg/cli/output/output.go @@ -22,7 +22,6 @@ import ( "io" "github.com/gosuri/uitable" - "github.com/pkg/errors" "sigs.k8s.io/yaml" ) @@ -73,7 +72,7 @@ func (o Format) Write(out io.Writer, w Writer) error { } // ParseFormat takes a raw string and returns the matching Format. -// If the format does not exists, ErrInvalidFormatType is returned +// If the format does not exist, ErrInvalidFormatType is returned func ParseFormat(s string) (out Format, err error) { switch s { case Table.String(): @@ -107,7 +106,7 @@ func EncodeJSON(out io.Writer, obj interface{}) error { enc := json.NewEncoder(out) err := enc.Encode(obj) if err != nil { - return errors.Wrap(err, "unable to write JSON output") + return fmt.Errorf("unable to write JSON output: %w", err) } return nil } @@ -117,12 +116,12 @@ func EncodeJSON(out io.Writer, obj interface{}) error { func EncodeYAML(out io.Writer, obj interface{}) error { raw, err := yaml.Marshal(obj) if err != nil { - return errors.Wrap(err, "unable to write YAML output") + return fmt.Errorf("unable to write YAML output: %w", err) } _, err = out.Write(raw) if err != nil { - return errors.Wrap(err, "unable to write YAML output") + return fmt.Errorf("unable to write YAML output: %w", err) } return nil } @@ -134,7 +133,7 @@ func EncodeTable(out io.Writer, table *uitable.Table) error { raw = append(raw, []byte("\n")...) _, err := out.Write(raw) if err != nil { - return errors.Wrap(err, "unable to write table output") + return fmt.Errorf("unable to write table output: %w", err) } return nil } diff --git a/pkg/helm/pkg/cli/values/options.go b/pkg/helm/pkg/cli/values/options.go index 5d94729c..1c99960f 100644 --- a/pkg/helm/pkg/cli/values/options.go +++ b/pkg/helm/pkg/cli/values/options.go @@ -17,19 +17,19 @@ limitations under the License. package values import ( + "bytes" "context" + "encoding/json" + "fmt" "io" "net/url" "os" "strings" - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - + "github.com/werf/nelm/pkg/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/strvals" - "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // Options captures the different ways to specify values @@ -44,51 +44,61 @@ type Options struct { // MergeValues merges values from files specified via -f/--values and directly // via --set-json, --set, --set-string, or --set-file, marshaling them to YAML -func (opts *Options) MergeValues(p getter.Providers, options helmopts.HelmOptions) (map[string]interface{}, error) { +func (opts *Options) MergeValues(ctx context.Context, p getter.Providers) (map[string]interface{}, error) { base := map[string]interface{}{} + ho := common.HelmOptionsFromContext(ctx) + // User specified a values files via -f/--values for _, filePath := range opts.ValueFiles { - currentMap := map[string]interface{}{} - - var bytes []byte + var raw []byte var err error - if options.ChartLoadOpts.ChartType == helmopts.ChartTypeChart && file.ChartFileReader != nil { - bytes, err = file.ChartFileReader.ReadChartFile(context.Background(), filePath) - if err != nil { - return nil, err - } - } else if data, err := readFile(filePath, p); err != nil { - return nil, err + if ho.ChartLoadOpts.ChartType == common.LegacyChartTypeChart && common.ChartFileReader != nil { + raw, err = common.ChartFileReader.ReadChartFile(ctx, filePath) } else { - bytes = data + raw, err = readFile(filePath, p) + } + if err != nil { + return nil, err } - if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { - return nil, errors.Wrapf(err, "failed to parse %s", filePath) + currentMap, err := loader.LoadValues(bytes.NewReader(raw)) + if err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", filePath, err) } // Merge with the previous map - base = mergeMaps(base, currentMap) + base = loader.MergeMaps(base, currentMap) } // User specified a value via --set-json for _, value := range opts.JSONValues { - if err := strvals.ParseJSON(value, base); err != nil { - return nil, errors.Errorf("failed parsing --set-json data %s", value) + trimmedValue := strings.TrimSpace(value) + if len(trimmedValue) > 0 && trimmedValue[0] == '{' { + // If value is JSON object format, parse it as map + var jsonMap map[string]interface{} + if err := json.Unmarshal([]byte(trimmedValue), &jsonMap); err != nil { + return nil, fmt.Errorf("failed parsing --set-json data JSON: %s", value) + } + base = loader.MergeMaps(base, jsonMap) + } else { + // Otherwise, parse it as key=value format + if err := strvals.ParseJSON(value, base); err != nil { + return nil, fmt.Errorf("failed parsing --set-json data %s", value) + } } } // User specified a value via --set for _, value := range opts.Values { if err := strvals.ParseInto(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set data") + return nil, fmt.Errorf("failed parsing --set data: %w", err) } } // User specified a value via --set-string for _, value := range opts.StringValues { if err := strvals.ParseIntoString(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set-string data") + return nil, fmt.Errorf("failed parsing --set-string data: %w", err) } } @@ -97,8 +107,8 @@ func (opts *Options) MergeValues(p getter.Providers, options helmopts.HelmOption reader := func(rs []rune) (interface{}, error) { var bytes []byte var err error - if options.ChartLoadOpts.ChartType == helmopts.ChartTypeChart && file.ChartFileReader != nil { - bytes, err = file.ChartFileReader.ReadChartFile(context.Background(), string(rs)) + if ho.ChartLoadOpts.ChartType == common.LegacyChartTypeChart && common.ChartFileReader != nil { + bytes, err = common.ChartFileReader.ReadChartFile(ctx, string(rs)) } else { bytes, err = readFile(string(rs), p) } @@ -109,39 +119,20 @@ func (opts *Options) MergeValues(p getter.Providers, options helmopts.HelmOption return string(bytes), nil } if err := strvals.ParseIntoFile(value, base, reader); err != nil { - return nil, errors.Wrap(err, "failed parsing --set-file data") + return nil, fmt.Errorf("failed parsing --set-file data: %w", err) } } // User specified a value via --set-literal for _, value := range opts.LiteralValues { if err := strvals.ParseLiteralInto(value, base); err != nil { - return nil, errors.Wrap(err, "failed parsing --set-literal data") + return nil, fmt.Errorf("failed parsing --set-literal data: %w", err) } } return base, nil } -func mergeMaps(a, b map[string]interface{}) map[string]interface{} { - out := make(map[string]interface{}, len(a)) - for k, v := range a { - out[k] = v - } - for k, v := range b { - if v, ok := v.(map[string]interface{}); ok { - if bv, ok := out[k]; ok { - if bv, ok := bv.(map[string]interface{}); ok { - out[k] = mergeMaps(bv, v) - continue - } - } - } - out[k] = v - } - return out -} - // readFile load a file from stdin, the local directory, or a remote file with a url. func readFile(filePath string, p getter.Providers) ([]byte, error) { if strings.TrimSpace(filePath) == "-" { @@ -161,5 +152,5 @@ func readFile(filePath string, p getter.Providers) ([]byte, error) { if err != nil { return nil, err } - return data.Bytes(), err + return data.Bytes(), nil } diff --git a/pkg/helm/pkg/cli/values/options_test.go b/pkg/helm/pkg/cli/values/options_test.go index 97df6ef8..5f036287 100644 --- a/pkg/helm/pkg/cli/values/options_test.go +++ b/pkg/helm/pkg/cli/values/options_test.go @@ -17,68 +17,276 @@ limitations under the License. package values import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "path/filepath" "reflect" + "strings" "testing" "github.com/werf/nelm/pkg/helm/pkg/getter" ) -func TestMergeValues(t *testing.T) { - nestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "stuff", - }, +// mockGetter implements getter.Getter for testing +type mockGetter struct { + content []byte + err error +} + +func (m *mockGetter) Get(_ string, _ ...getter.Option) (*bytes.Buffer, error) { + if m.err != nil { + return nil, m.err } - anotherNestedMap := map[string]interface{}{ - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", + return bytes.NewBuffer(m.content), nil +} + +// mockProvider creates a test provider +func mockProvider(schemes []string, content []byte, err error) getter.Provider { + return getter.Provider{ + Schemes: schemes, + New: func(_ ...getter.Option) (getter.Getter, error) { + return &mockGetter{content: content, err: err}, nil }, } - flatMap := map[string]interface{}{ - "foo": "bar", - "baz": "stuff", - } - anotherFlatMap := map[string]interface{}{ - "testing": "fun", - } +} - testMap := mergeMaps(flatMap, nestedMap) - equal := reflect.DeepEqual(testMap, nestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite a flat value. Expected: %v, got %v", nestedMap, testMap) +func TestReadFile(t *testing.T) { + tests := []struct { + name string + filePath string + providers getter.Providers + setupFunc func(*testing.T) (string, func()) // setup temp files, return cleanup + expectError bool + expectStdin bool + expectedData []byte + }{ + { + name: "stdin input with dash", + filePath: "-", + providers: getter.Providers{}, + expectStdin: true, + expectError: false, + }, + { + name: "stdin input with whitespace", + filePath: " - ", + providers: getter.Providers{}, + expectStdin: true, + expectError: false, + }, + { + name: "invalid URL parsing", + filePath: "://invalid-url", + providers: getter.Providers{}, + expectError: true, + }, + { + name: "local file - existing", + filePath: "test.txt", + providers: getter.Providers{}, + setupFunc: func(t *testing.T) (string, func()) { + t.Helper() + tmpDir := t.TempDir() + filePath := filepath.Join(tmpDir, "test.txt") + content := []byte("local file content") + err := os.WriteFile(filePath, content, 0644) + if err != nil { + t.Fatal(err) + } + return filePath, func() {} // cleanup handled by t.TempDir() + }, + expectError: false, + expectedData: []byte("local file content"), + }, + { + name: "local file - non-existent", + filePath: "/non/existent/file.txt", + providers: getter.Providers{}, + expectError: true, + }, + { + name: "remote file with http scheme - success", + filePath: "http://example.com/values.yaml", + providers: getter.Providers{ + mockProvider([]string{"http", "https"}, []byte("remote content"), nil), + }, + expectError: false, + expectedData: []byte("remote content"), + }, + { + name: "remote file with https scheme - success", + filePath: "https://example.com/values.yaml", + providers: getter.Providers{ + mockProvider([]string{"http", "https"}, []byte("https content"), nil), + }, + expectError: false, + expectedData: []byte("https content"), + }, + { + name: "remote file with custom scheme - success", + filePath: "oci://registry.example.com/chart", + providers: getter.Providers{ + mockProvider([]string{"oci"}, []byte("oci content"), nil), + }, + expectError: false, + expectedData: []byte("oci content"), + }, + { + name: "remote file - getter error", + filePath: "http://example.com/values.yaml", + providers: getter.Providers{ + mockProvider([]string{"http"}, nil, errors.New("network error")), + }, + expectError: true, + }, + { + name: "unsupported scheme fallback to local file", + filePath: "ftp://example.com/file.txt", + providers: getter.Providers{ + mockProvider([]string{"http"}, []byte("should not be used"), nil), + }, + setupFunc: func(t *testing.T) (string, func()) { + t.Helper() + // Create a local file named "ftp://example.com/file.txt" + // This tests the fallback behavior when scheme is not supported + tmpDir := t.TempDir() + fileName := "ftp_file.txt" // Valid filename for filesystem + filePath := filepath.Join(tmpDir, fileName) + content := []byte("local fallback content") + err := os.WriteFile(filePath, content, 0644) + if err != nil { + t.Fatal(err) + } + return filePath, func() {} + }, + expectError: false, + expectedData: []byte("local fallback content"), + }, + { + name: "empty file path", + filePath: "", + providers: getter.Providers{}, + expectError: true, // Empty path should cause error + }, + { + name: "multiple providers - correct selection", + filePath: "custom://example.com/resource", + providers: getter.Providers{ + mockProvider([]string{"http", "https"}, []byte("wrong content"), nil), + mockProvider([]string{"custom"}, []byte("correct content"), nil), + mockProvider([]string{"oci"}, []byte("also wrong"), nil), + }, + expectError: false, + expectedData: []byte("correct content"), + }, } - testMap = mergeMaps(nestedMap, flatMap) - equal = reflect.DeepEqual(testMap, flatMap) - if !equal { - t.Errorf("Expected a flat value to overwrite a map. Expected: %v, got %v", flatMap, testMap) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actualFilePath string + var cleanup func() + + if tt.setupFunc != nil { + actualFilePath, cleanup = tt.setupFunc(t) + defer cleanup() + } else { + actualFilePath = tt.filePath + } - testMap = mergeMaps(nestedMap, anotherNestedMap) - equal = reflect.DeepEqual(testMap, anotherNestedMap) - if !equal { - t.Errorf("Expected a nested map to overwrite another nested map. Expected: %v, got %v", anotherNestedMap, testMap) + // Handle stdin test case + if tt.expectStdin { + // Save original stdin + originalStdin := os.Stdin + defer func() { os.Stdin = originalStdin }() + + // Create a pipe for stdin + r, w, err := os.Pipe() + if err != nil { + t.Fatal(err) + } + defer r.Close() + defer w.Close() + + // Replace stdin with our pipe + os.Stdin = r + + // Write test data to stdin + testData := []byte("stdin test data") + go func() { + defer w.Close() + w.Write(testData) + }() + + // Test the function + got, err := readFile(actualFilePath, tt.providers) + if err != nil { + t.Errorf("readFile() error = %v, expected no error for stdin", err) + return + } + + if !bytes.Equal(got, testData) { + t.Errorf("readFile() = %v, want %v", got, testData) + } + return + } + + // Regular test cases + got, err := readFile(actualFilePath, tt.providers) + if (err != nil) != tt.expectError { + t.Errorf("readFile() error = %v, expectError %v", err, tt.expectError) + return + } + + if !tt.expectError && tt.expectedData != nil { + if !bytes.Equal(got, tt.expectedData) { + t.Errorf("readFile() = %v, want %v", got, tt.expectedData) + } + } + }) } +} - testMap = mergeMaps(anotherFlatMap, anotherNestedMap) - expectedMap := map[string]interface{}{ - "testing": "fun", - "foo": "bar", - "baz": map[string]string{ - "cool": "things", - "awesome": "stuff", +// TestReadFileErrorMessages tests specific error scenarios and their messages +func TestReadFileErrorMessages(t *testing.T) { + tests := []struct { + name string + filePath string + providers getter.Providers + wantErr string + }{ + { + name: "URL parse error", + filePath: "://invalid", + providers: getter.Providers{}, + wantErr: "missing protocol scheme", + }, + { + name: "getter error with message", + filePath: "http://example.com/file", + providers: getter.Providers{mockProvider([]string{"http"}, nil, fmt.Errorf("connection refused"))}, + wantErr: "connection refused", }, } - equal = reflect.DeepEqual(testMap, expectedMap) - if !equal { - t.Errorf("Expected a map with different keys to merge properly with another map. Expected: %v, got %v", expectedMap, testMap) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := readFile(tt.filePath, tt.providers) + if err == nil { + t.Errorf("readFile() expected error containing %q, got nil", tt.wantErr) + return + } + if !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("readFile() error = %v, want error containing %q", err, tt.wantErr) + } + }) } } -func TestReadFile(t *testing.T) { +// Original test case - keeping for backward compatibility +func TestReadFileOriginal(t *testing.T) { var p getter.Providers filePath := "%a.txt" _, err := readFile(filePath, p) @@ -86,3 +294,97 @@ func TestReadFile(t *testing.T) { t.Errorf("Expected error when has special strings") } } + +func TestMergeValuesCLI(t *testing.T) { + tests := []struct { + name string + opts Options + expected map[string]interface{} + wantErr bool + }{ + { + name: "set-json object", + opts: Options{ + JSONValues: []string{`{"foo": {"bar": "baz"}}`}, + }, + expected: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": "baz", + }, + }, + }, + { + name: "set-json key=value", + opts: Options{ + JSONValues: []string{"foo.bar=[1,2,3]"}, + }, + expected: map[string]interface{}{ + "foo": map[string]interface{}{ + "bar": []interface{}{1.0, 2.0, 3.0}, + }, + }, + }, + { + name: "set regular value", + opts: Options{ + Values: []string{"foo=bar"}, + }, + expected: map[string]interface{}{ + "foo": "bar", + }, + }, + { + name: "set string value", + opts: Options{ + StringValues: []string{"foo=123"}, + }, + expected: map[string]interface{}{ + "foo": "123", + }, + }, + { + name: "set literal value", + opts: Options{ + LiteralValues: []string{"foo=true"}, + }, + expected: map[string]interface{}{ + "foo": "true", + }, + }, + { + name: "multiple options", + opts: Options{ + Values: []string{"a=foo"}, + StringValues: []string{"b=bar"}, + JSONValues: []string{`{"c": "foo1"}`}, + LiteralValues: []string{"d=bar1"}, + }, + expected: map[string]interface{}{ + "a": "foo", + "b": "bar", + "c": "foo1", + "d": "bar1", + }, + }, + { + name: "invalid json", + opts: Options{ + JSONValues: []string{`{invalid`}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.opts.MergeValues(context.Background(), getter.Providers{}) + if (err != nil) != tt.wantErr { + t.Errorf("MergeValues() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.expected) { + t.Errorf("MergeValues() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/pkg/helm/cmd/helm/completion.go b/pkg/helm/pkg/cmd/completion.go similarity index 83% rename from pkg/helm/cmd/helm/completion.go rename to pkg/helm/pkg/cmd/completion.go index 4be6e927..abf4f18a 100644 --- a/pkg/helm/cmd/helm/completion.go +++ b/pkg/helm/pkg/cmd/completion.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" @@ -23,7 +23,7 @@ import ( "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" ) const completionDesc = ` @@ -102,8 +102,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command { Short: "generate autocompletion script for bash", Long: bashCompDesc, Args: require.NoArgs, - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(cmd *cobra.Command, _ []string) error { return runCompletionBash(out, cmd) }, } @@ -114,8 +114,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command { Short: "generate autocompletion script for zsh", Long: zshCompDesc, Args: require.NoArgs, - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(cmd *cobra.Command, _ []string) error { return runCompletionZsh(out, cmd) }, } @@ -126,8 +126,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command { Short: "generate autocompletion script for fish", Long: fishCompDesc, Args: require.NoArgs, - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(cmd *cobra.Command, _ []string) error { return runCompletionFish(out, cmd) }, } @@ -138,8 +138,8 @@ func newCompletionCmd(out io.Writer) *cobra.Command { Short: "generate autocompletion script for powershell", Long: powershellCompDesc, Args: require.NoArgs, - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(cmd *cobra.Command, _ []string) error { return runCompletionPowershell(out, cmd) }, } @@ -209,7 +209,15 @@ func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error { return cmd.Root().GenPowerShellCompletionWithDesc(out) } -// Function to disable file completion -func noCompletions(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return nil, cobra.ShellCompDirectiveNoFileComp +// noMoreArgsCompFunc deactivates file completion when doing argument shell completion. +// It also provides some ActiveHelp to indicate no more arguments are accepted. +func noMoreArgsCompFunc(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return noMoreArgsComp() +} + +// noMoreArgsComp deactivates file completion when doing argument shell completion. +// It also provides some ActiveHelp to indicate no more arguments are accepted. +func noMoreArgsComp() ([]string, cobra.ShellCompDirective) { + activeHelpMsg := "This command does not take any more arguments (but may accept flags)." + return cobra.AppendActiveHelp(nil, activeHelpMsg), cobra.ShellCompDirectiveNoFileComp } diff --git a/pkg/helm/cmd/helm/completion_test.go b/pkg/helm/pkg/cmd/completion_test.go similarity index 91% rename from pkg/helm/cmd/helm/completion_test.go rename to pkg/helm/pkg/cmd/completion_test.go index 88f22ff3..f91da976 100644 --- a/pkg/helm/cmd/helm/completion_test.go +++ b/pkg/helm/pkg/cmd/completion_test.go @@ -14,23 +14,25 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" "strings" "testing" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/release" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) // Check if file completion should be performed according to parameter 'shouldBePerformed' func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) { + t.Helper() storage := storageFixture() storage.Create(&release.Release{ Name: "myrelease", - Info: &release.Info{Status: release.StatusDeployed}, + Info: &release.Info{Status: common.StatusDeployed}, Chart: &chart.Chart{ Metadata: &chart.Metadata{ Name: "Myrelease-Chart", @@ -64,6 +66,7 @@ func TestCompletionFileCompletion(t *testing.T) { } func checkReleaseCompletion(t *testing.T, cmdName string, multiReleasesAllowed bool) { + t.Helper() multiReleaseTestGolden := "output/empty_nofile_comp.txt" if multiReleasesAllowed { multiReleaseTestGolden = "output/release_list_repeat_comp.txt" diff --git a/pkg/helm/pkg/cmd/create.go b/pkg/helm/pkg/cmd/create.go new file mode 100644 index 00000000..8b014474 --- /dev/null +++ b/pkg/helm/pkg/cmd/create.go @@ -0,0 +1,113 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + "path/filepath" + + "github.com/spf13/cobra" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + "github.com/werf/nelm/pkg/helm/pkg/helmpath" +) + +const createDesc = ` +This command creates a chart directory along with the common files and +directories used in a chart. + +For example, 'helm create foo' will create a directory structure that looks +something like this: + + foo/ + ├── .helmignore # Contains patterns to ignore when packaging Helm charts. + ├── Chart.yaml # Information about your chart + ├── values.yaml # The default values for your templates + ├── charts/ # Charts that this chart depends on + └── templates/ # The template files + └── tests/ # The test files + +'helm create' takes a path for an argument. If directories in the given path +do not exist, Helm will attempt to create them as it goes. If the given +destination exists and there are files in that directory, conflicting files +will be overwritten, but other files will be left alone. +` + +type createOptions struct { + starter string // --starter + name string + starterDir string +} + +func newCreateCmd(out io.Writer) *cobra.Command { + o := &createOptions{} + + cmd := &cobra.Command{ + Use: "create NAME", + Short: "create a new chart with the given name", + Long: createDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + // Allow file completion when completing the argument for the name + // which could be a path + return nil, cobra.ShellCompDirectiveDefault + } + // No more completions, so disable file completion + return noMoreArgsComp() + }, + RunE: func(_ *cobra.Command, args []string) error { + o.name = args[0] + o.starterDir = helmpath.DataPath("starters") + return o.run(out) + }, + } + + cmd.Flags().StringVarP(&o.starter, "starter", "p", "", "the name or absolute path to Helm starter scaffold") + return cmd +} + +func (o *createOptions) run(out io.Writer) error { + fmt.Fprintf(out, "Creating %s\n", o.name) + + chartname := filepath.Base(o.name) + cfile := &chart.Metadata{ + Name: chartname, + Description: "A Helm chart for Kubernetes", + Type: "application", + Version: "0.1.0", + AppVersion: "0.1.0", + APIVersion: chart.APIVersionV2, + } + + if o.starter != "" { + // Create from the starter + lstarter := filepath.Join(o.starterDir, o.starter) + // If path is absolute, we don't want to prefix it with helm starters folder + if filepath.IsAbs(o.starter) { + lstarter = o.starter + } + return chartutil.CreateFrom(cfile, filepath.Dir(o.name), lstarter) + } + + chartutil.Stderr = out + _, err := chartutil.Create(chartname, filepath.Dir(o.name)) + return err +} diff --git a/pkg/helm/pkg/cmd/create_test.go b/pkg/helm/pkg/cmd/create_test.go new file mode 100644 index 00000000..7c845acd --- /dev/null +++ b/pkg/helm/pkg/cmd/create_test.go @@ -0,0 +1,193 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/werf/nelm/pkg/helm/intern/test/ensure" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" + "github.com/werf/nelm/pkg/helm/pkg/helmpath" +) + +func TestCreateCmd(t *testing.T) { + t.Chdir(t.TempDir()) + ensure.HelmHome(t) + cname := "testchart" + + // Run a create + if _, _, err := executeActionCommand("create " + cname); err != nil { + t.Fatalf("Failed to run create: %s", err) + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := loader.LoadDir(context.Background(), cname) + if err != nil { + t.Fatal(err) + } + + if c.Name() != cname { + t.Errorf("Expected %q name, got %q", cname, c.Name()) + } + if c.Metadata.APIVersion != chart.APIVersionV2 { + t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) + } +} + +func TestCreateStarterCmd(t *testing.T) { + t.Chdir(t.TempDir()) + ensure.HelmHome(t) + cname := "testchart" + defer resetEnv()() + // Create a starter. + starterchart := helmpath.DataPath("starters") + os.MkdirAll(starterchart, 0o755) + if dest, err := chartutil.Create("starterchart", starterchart); err != nil { + t.Fatalf("Could not create chart: %s", err) + } else { + t.Logf("Created %s", dest) + } + tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") + if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil { + t.Fatalf("Could not write template: %s", err) + } + + // Run a create + if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=starterchart %s", cname)); err != nil { + t.Errorf("Failed to run create: %s", err) + return + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := loader.LoadDir(context.Background(), cname) + if err != nil { + t.Fatal(err) + } + + if c.Name() != cname { + t.Errorf("Expected %q name, got %q", cname, c.Name()) + } + if c.Metadata.APIVersion != chart.APIVersionV2 { + t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) + } + + expectedNumberOfTemplates := 10 + if l := len(c.Templates); l != expectedNumberOfTemplates { + t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l) + } + + found := false + for _, tpl := range c.Templates { + if tpl.Name == "templates/foo.tpl" { + found = true + if data := string(tpl.Data); data != "test" { + t.Errorf("Expected template 'test', got %q", data) + } + } + } + if !found { + t.Error("Did not find foo.tpl") + } +} + +func TestCreateStarterAbsoluteCmd(t *testing.T) { + t.Chdir(t.TempDir()) + defer resetEnv()() + ensure.HelmHome(t) + cname := "testchart" + + // Create a starter. + starterchart := helmpath.DataPath("starters") + os.MkdirAll(starterchart, 0o755) + if dest, err := chartutil.Create("starterchart", starterchart); err != nil { + t.Fatalf("Could not create chart: %s", err) + } else { + t.Logf("Created %s", dest) + } + tplpath := filepath.Join(starterchart, "starterchart", "templates", "foo.tpl") + if err := os.WriteFile(tplpath, []byte("test"), 0o644); err != nil { + t.Fatalf("Could not write template: %s", err) + } + + starterChartPath := filepath.Join(starterchart, "starterchart") + + // Run a create + if _, _, err := executeActionCommand(fmt.Sprintf("create --starter=%s %s", starterChartPath, cname)); err != nil { + t.Errorf("Failed to run create: %s", err) + return + } + + // Test that the chart is there + if fi, err := os.Stat(cname); err != nil { + t.Fatalf("no chart directory: %s", err) + } else if !fi.IsDir() { + t.Fatalf("chart is not directory") + } + + c, err := loader.LoadDir(context.Background(), cname) + if err != nil { + t.Fatal(err) + } + + if c.Name() != cname { + t.Errorf("Expected %q name, got %q", cname, c.Name()) + } + if c.Metadata.APIVersion != chart.APIVersionV2 { + t.Errorf("Wrong API version: %q", c.Metadata.APIVersion) + } + + expectedNumberOfTemplates := 10 + if l := len(c.Templates); l != expectedNumberOfTemplates { + t.Errorf("Expected %d templates, got %d", expectedNumberOfTemplates, l) + } + + found := false + for _, tpl := range c.Templates { + if tpl.Name == "templates/foo.tpl" { + found = true + if data := string(tpl.Data); data != "test" { + t.Errorf("Expected template 'test', got %q", data) + } + } + } + if !found { + t.Error("Did not find foo.tpl") + } +} + +func TestCreateFileCompletion(t *testing.T) { + checkFileCompletion(t, "create", true) + checkFileCompletion(t, "create myname", false) +} diff --git a/pkg/helm/pkg/cmd/dependency.go b/pkg/helm/pkg/cmd/dependency.go new file mode 100644 index 00000000..95281df4 --- /dev/null +++ b/pkg/helm/pkg/cmd/dependency.go @@ -0,0 +1,136 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "io" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" +) + +const dependencyDesc = ` +Manage the dependencies of a chart. + +Helm charts store their dependencies in 'charts/'. For chart developers, it is +often easier to manage dependencies in 'Chart.yaml' which declares all +dependencies. + +The dependency commands operate on that file, making it easy to synchronize +between the desired dependencies and the actual dependencies stored in the +'charts/' directory. + +For example, this Chart.yaml declares two dependencies: + + # Chart.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "https://example.com/charts" + - name: memcached + version: "3.2.1" + repository: "https://another.example.com/charts" + + +The 'name' should be the name of a chart, where that name must match the name +in that chart's 'Chart.yaml' file. + +The 'version' field should contain a semantic version or version range. + +The 'repository' URL should point to a Chart Repository. Helm expects that by +appending '/index.yaml' to the URL, it should be able to retrieve the chart +repository's index. Note: 'repository' can be an alias. The alias must start +with 'alias:' or '@'. + +Starting from 2.2.0, repository can be defined as the path to the directory of +the dependency charts stored locally. The path should start with a prefix of +"file://". For example, + + # Chart.yaml + dependencies: + - name: nginx + version: "1.2.3" + repository: "file://../dependency_chart/nginx" + +If the dependency chart is retrieved locally, it is not required to have the +repository added to helm by "helm add repo". Version matching is also supported +for this case. +` + +const dependencyListDesc = ` +List all of the dependencies declared in a chart. + +This can take chart archives and chart directories as input. It will not alter +the contents of a chart. + +This will produce an error if the chart cannot be loaded. +` + +func newDependencyCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "dependency update|build|list", + Aliases: []string{"dep", "dependencies"}, + Short: "manage a chart's dependencies", + Long: dependencyDesc, + Args: require.NoArgs, + } + + cmd.AddCommand(newDependencyListCmd(out)) + cmd.AddCommand(newDependencyUpdateCmd(cfg, out)) + cmd.AddCommand(newDependencyBuildCmd(out)) + + return cmd +} + +func newDependencyListCmd(out io.Writer) *cobra.Command { + client := action.NewDependency() + cmd := &cobra.Command{ + Use: "list CHART", + Aliases: []string{"ls"}, + Short: "list the dependencies for the given chart", + Long: dependencyListDesc, + Args: require.MaximumNArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + chartpath := "." + if len(args) > 0 { + chartpath = filepath.Clean(args[0]) + } + return client.List(chartpath, out) + }, + } + + f := cmd.Flags() + + f.UintVar(&client.ColumnWidth, "max-col-width", 80, "maximum column width for output table") + return cmd +} + +func addDependencySubcommandFlags(f *pflag.FlagSet, client *action.Dependency) { + f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") + f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.BoolVar(&client.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") + f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") + f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") +} diff --git a/pkg/helm/cmd/helm/dependency_build.go b/pkg/helm/pkg/cmd/dependency_build.go similarity index 76% rename from pkg/helm/cmd/helm/dependency_build.go rename to pkg/helm/pkg/cmd/dependency_build.go index cc75749e..923abe7f 100644 --- a/pkg/helm/cmd/helm/dependency_build.go +++ b/pkg/helm/pkg/cmd/dependency_build.go @@ -13,9 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "context" "fmt" "io" "os" @@ -24,11 +25,10 @@ import ( "github.com/spf13/cobra" "k8s.io/client-go/util/homedir" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" "github.com/werf/nelm/pkg/helm/pkg/downloader" "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) const dependencyBuildDesc = ` @@ -42,7 +42,7 @@ If no lock file is found, 'helm dependency build' will mirror the behavior of 'helm dependency update'. ` -func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { +func newDependencyBuildCmd(out io.Writer) *cobra.Command { client := action.NewDependency() cmd := &cobra.Command{ @@ -50,34 +50,33 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm Short: "rebuild the charts/ directory based on the Chart.lock file", Long: dependencyBuildDesc, Args: require.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { chartpath := "." if len(args) > 0 { chartpath = filepath.Clean(args[0]) } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + man := &downloader.Manager{ Out: out, ChartPath: chartpath, Keyring: client.Keyring, SkipUpdate: client.SkipRefresh, Getters: getter.All(settings), - RegistryClient: cfg.RegistryClient, + RegistryClient: registryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, + ContentCache: settings.ContentCache, Debug: settings.Debug, } if client.Verify { man.Verify = downloader.VerifyIfPossible } - - opts := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ - DepDownloader: man, - NoSecrets: true, - }, - } - - err := man.Build(opts) + err = man.Build(context.Background()) if e, ok := err.(downloader.ErrRepoNotFound); ok { return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error()) } @@ -86,9 +85,7 @@ func newDependencyBuildCmd(cfg *action.Configuration, out io.Writer) *cobra.Comm } f := cmd.Flags() - f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") - f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + addDependencySubcommandFlags(f, client) return cmd } diff --git a/pkg/helm/cmd/helm/dependency_build_test.go b/pkg/helm/pkg/cmd/dependency_build_test.go similarity index 87% rename from pkg/helm/cmd/helm/dependency_build_test.go rename to pkg/helm/pkg/cmd/dependency_build_test.go index c0204721..3971ba7c 100644 --- a/pkg/helm/cmd/helm/dependency_build_test.go +++ b/pkg/helm/pkg/cmd/dependency_build_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" @@ -22,18 +22,18 @@ import ( "strings" "testing" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/provenance" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" ) func TestDependencyBuildCmd(t *testing.T) { - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"), + ) defer srv.Stop() - if err != nil { - t.Fatal(err) - } rootDir := srv.Root() srv.LinkIndices() @@ -58,7 +58,7 @@ func TestDependencyBuildCmd(t *testing.T) { createTestingChart(t, rootDir, chartname, srv.URL()) repoFile := filepath.Join(rootDir, "repositories.yaml") - cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) + cmd := fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --plain-http", filepath.Join(rootDir, chartname), repoFile, rootDir) _, out, err := executeActionCommand(cmd) // In the first pass, we basically want the same results as an update. @@ -117,7 +117,7 @@ func TestDependencyBuildCmd(t *testing.T) { t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v) } - skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s", filepath.Join(rootDir, chartname), repoFile, rootDir) + skipRefreshCmd := fmt.Sprintf("dependency build '%s' --skip-refresh --repository-config %s --repository-cache %s --plain-http", filepath.Join(rootDir, chartname), repoFile, rootDir) _, out, err = executeActionCommand(skipRefreshCmd) // In this pass, we check --skip-refresh option becomes effective. @@ -134,7 +134,7 @@ func TestDependencyBuildCmd(t *testing.T) { if err := chartutil.SaveDir(c, dir()); err != nil { t.Fatal(err) } - cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", + cmd = fmt.Sprintf("dependency build '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json --plain-http", dir(ociChartName), dir("repositories.yaml"), dir(), diff --git a/pkg/helm/pkg/cmd/dependency_test.go b/pkg/helm/pkg/cmd/dependency_test.go new file mode 100644 index 00000000..d6bcebf1 --- /dev/null +++ b/pkg/helm/pkg/cmd/dependency_test.go @@ -0,0 +1,57 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "runtime" + "testing" +) + +func TestDependencyListCmd(t *testing.T) { + noSuchChart := cmdTestCase{ + name: "No such chart", + cmd: "dependency list /no/such/chart", + golden: "output/dependency-list-no-chart-linux.txt", + wantError: true, + } + + noDependencies := cmdTestCase{ + name: "No dependencies", + cmd: "dependency list testdata/testcharts/alpine", + golden: "output/dependency-list-no-requirements-linux.txt", + } + + if runtime.GOOS == "windows" { + noSuchChart.golden = "output/dependency-list-no-chart-windows.txt" + noDependencies.golden = "output/dependency-list-no-requirements-windows.txt" + } + + tests := []cmdTestCase{noSuchChart, + noDependencies, { + name: "Dependencies in chart dir", + cmd: "dependency list testdata/testcharts/reqtest", + golden: "output/dependency-list.txt", + }, { + name: "Dependencies in chart archive", + cmd: "dependency list testdata/testcharts/reqtest-0.1.0.tgz", + golden: "output/dependency-list-archive.txt", + }} + runTestCmd(t, tests) +} + +func TestDependencyFileCompletion(t *testing.T) { + checkFileCompletion(t, "dependency", false) +} diff --git a/pkg/helm/cmd/helm/dependency_update.go b/pkg/helm/pkg/cmd/dependency_update.go similarity index 75% rename from pkg/helm/cmd/helm/dependency_update.go rename to pkg/helm/pkg/cmd/dependency_update.go index f6daa45c..a89ba707 100644 --- a/pkg/helm/cmd/helm/dependency_update.go +++ b/pkg/helm/pkg/cmd/dependency_update.go @@ -13,19 +13,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "context" + "fmt" "io" "path/filepath" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" "github.com/werf/nelm/pkg/helm/pkg/downloader" "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) const dependencyUpDesc = ` @@ -44,7 +45,7 @@ in the Chart.yaml file, but (b) at the wrong version. ` // newDependencyUpdateCmd creates a new dependency update command. -func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { +func newDependencyUpdateCmd(_ *action.Configuration, out io.Writer) *cobra.Command { client := action.NewDependency() cmd := &cobra.Command{ @@ -53,41 +54,38 @@ func newDependencyUpdateCmd(cfg *action.Configuration, out io.Writer) *cobra.Com Short: "update charts/ based on the contents of Chart.yaml", Long: dependencyUpDesc, Args: require.MaximumNArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { chartpath := "." if len(args) > 0 { chartpath = filepath.Clean(args[0]) } + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + man := &downloader.Manager{ Out: out, ChartPath: chartpath, Keyring: client.Keyring, SkipUpdate: client.SkipRefresh, Getters: getter.All(settings), - RegistryClient: cfg.RegistryClient, + RegistryClient: registryClient, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, + ContentCache: settings.ContentCache, Debug: settings.Debug, } if client.Verify { man.Verify = downloader.VerifyAlways } - - opts := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ - DepDownloader: man, - NoSecrets: true, - }, - } - - return man.Update(opts) + return man.Update(context.Background()) }, } f := cmd.Flags() - f.BoolVar(&client.Verify, "verify", false, "verify the packages against signatures") - f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "keyring containing public keys") - f.BoolVar(&client.SkipRefresh, "skip-refresh", false, "do not refresh the local repository cache") + addDependencySubcommandFlags(f, client) return cmd } diff --git a/pkg/helm/cmd/helm/dependency_update_test.go b/pkg/helm/pkg/cmd/dependency_update_test.go similarity index 82% rename from pkg/helm/cmd/helm/dependency_update_test.go rename to pkg/helm/pkg/cmd/dependency_update_test.go index d93a5485..3f57c662 100644 --- a/pkg/helm/cmd/helm/dependency_update_test.go +++ b/pkg/helm/pkg/cmd/dependency_update_test.go @@ -13,29 +13,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" "testing" "github.com/werf/nelm/pkg/helm/intern/test/ensure" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/provenance" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" ) func TestDependencyUpdateCmd(t *testing.T) { - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"), + ) defer srv.Stop() t.Logf("Listening on directory %s", srv.Root()) @@ -43,6 +45,7 @@ func TestDependencyUpdateCmd(t *testing.T) { if err != nil { t.Fatal(err) } + contentCache := t.TempDir() ociChartName := "oci-depending-chart" c := createTestingMetadataForOCI(ociChartName, ociSrv.RegistryURL) @@ -67,7 +70,7 @@ func TestDependencyUpdateCmd(t *testing.T) { } _, out, err := executeActionCommand( - fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir()), + fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --content-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir(), contentCache), ) if err != nil { t.Logf("Output: %s", out) @@ -110,7 +113,7 @@ func TestDependencyUpdateCmd(t *testing.T) { t.Fatal(err) } - _, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + _, out, err = executeActionCommand(fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --content-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir(), contentCache)) if err != nil { t.Logf("Output: %s", out) t.Fatal(err) @@ -131,11 +134,12 @@ func TestDependencyUpdateCmd(t *testing.T) { if err := chartutil.SaveDir(c, dir()); err != nil { t.Fatal(err) } - cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json", + cmd := fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --registry-config %s/config.json --content-cache %s --plain-http", dir(ociChartName), dir("repositories.yaml"), dir(), - dir()) + dir(), + contentCache) _, out, err = executeActionCommand(cmd) if err != nil { t.Logf("Output: %s", out) @@ -151,10 +155,10 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { defer resetEnv()() ensure.HelmHome(t) - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"), + ) defer srv.Stop() t.Logf("Listening on directory %s", srv.Root()) @@ -169,7 +173,7 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { } createTestingChart(t, dir(), chartname, srv.URL()) - _, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + _, output, err := executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir())) if err != nil { t.Logf("Output: %s", output) t.Fatal(err) @@ -177,8 +181,9 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { // Chart repo is down srv.Stop() + contentCache := t.TempDir() - _, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s", dir(chartname), dir("repositories.yaml"), dir())) + _, output, err = executeActionCommand(fmt.Sprintf("dependency update %s --repository-config %s --repository-cache %s --content-cache %s --plain-http", dir(chartname), dir("repositories.yaml"), dir(), contentCache)) if err == nil { t.Logf("Output: %s", output) t.Fatal("Expected error, got nil") @@ -200,8 +205,9 @@ func TestDependencyUpdateCmd_DoNotDeleteOldChartsOnError(t *testing.T) { } } - // Make sure tmpcharts is deleted - if _, err := os.Stat(filepath.Join(dir(chartname), "tmpcharts")); !os.IsNotExist(err) { + // Make sure tmpcharts-x is deleted + tmpPath := filepath.Join(dir(chartname), fmt.Sprintf("tmpcharts-%d", os.Getpid())) + if _, err := os.Stat(tmpPath); !errors.Is(err, fs.ErrNotExist) { t.Fatalf("tmpcharts dir still exists") } } @@ -229,9 +235,11 @@ func TestDependencyUpdateCmd_WithRepoThatWasNotAdded(t *testing.T) { t.Fatal(err) } + contentCache := t.TempDir() + _, out, err := executeActionCommand( - fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s", dir(chartname), - dir("repositories.yaml"), dir()), + fmt.Sprintf("dependency update '%s' --repository-config %s --repository-cache %s --content-cache %s", dir(chartname), + dir("repositories.yaml"), dir(), contentCache), ) if err != nil { @@ -247,10 +255,11 @@ func TestDependencyUpdateCmd_WithRepoThatWasNotAdded(t *testing.T) { } func setupMockRepoServer(t *testing.T) *repotest.Server { - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz") - if err != nil { - t.Fatal(err) - } + t.Helper() + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz"), + ) t.Logf("Listening on directory %s", srv.Root()) diff --git a/pkg/helm/pkg/cmd/env.go b/pkg/helm/pkg/cmd/env.go new file mode 100644 index 00000000..91592529 --- /dev/null +++ b/pkg/helm/pkg/cmd/env.go @@ -0,0 +1,76 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + "sort" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" +) + +var envHelp = ` +Env prints out all the environment information in use by Helm. +` + +func newEnvCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "env", + Short: "helm client environment information", + Long: envHelp, + Args: require.MaximumNArgs(1), + ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + keys := getSortedEnvVarKeys() + return keys, cobra.ShellCompDirectiveNoFileComp + } + + return noMoreArgsComp() + }, + Run: func(_ *cobra.Command, args []string) { + envVars := settings.EnvVars() + + if len(args) == 0 { + // Sort the variables by alphabetical order. + // This allows for a constant output across calls to 'helm env'. + keys := getSortedEnvVarKeys() + + for _, k := range keys { + fmt.Fprintf(out, "%s=\"%s\"\n", k, envVars[k]) + } + } else { + fmt.Fprintf(out, "%s\n", envVars[args[0]]) + } + }, + } + return cmd +} + +func getSortedEnvVarKeys() []string { + envVars := settings.EnvVars() + + var keys []string + for k := range envVars { + keys = append(keys, k) + } + sort.Strings(keys) + + return keys +} diff --git a/pkg/helm/pkg/cmd/env_test.go b/pkg/helm/pkg/cmd/env_test.go new file mode 100644 index 00000000..c5d7af1b --- /dev/null +++ b/pkg/helm/pkg/cmd/env_test.go @@ -0,0 +1,35 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestEnv(t *testing.T) { + tests := []cmdTestCase{{ + name: "completion for env", + cmd: "__complete env ''", + golden: "output/env-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestEnvFileCompletion(t *testing.T) { + checkFileCompletion(t, "env", false) + checkFileCompletion(t, "env HELM_BIN", false) +} diff --git a/pkg/helm/pkg/cmd/flags.go b/pkg/helm/pkg/cmd/flags.go new file mode 100644 index 00000000..d77f889f --- /dev/null +++ b/pkg/helm/pkg/cmd/flags.go @@ -0,0 +1,218 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "flag" + "fmt" + "log" + "log/slog" + "path/filepath" + "sort" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "k8s.io/klog/v2" + + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/cli/values" + "github.com/werf/nelm/pkg/helm/pkg/helmpath" + "github.com/werf/nelm/pkg/helm/pkg/kube" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +const ( + outputFlag = "output" +) + +func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) { + f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)") + f.StringArrayVar(&v.Values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.StringValues, "set-string", []string{}, "set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") + f.StringArrayVar(&v.FileValues, "set-file", []string{}, "set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") + f.StringArrayVar(&v.JSONValues, "set-json", []string{}, "set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2 or using json format: {\"key1\": jsonval1, \"key2\": \"jsonval2\"})") + f.StringArrayVar(&v.LiteralValues, "set-literal", []string{}, "set a literal STRING value on the command line") +} + +func AddWaitFlag(cmd *cobra.Command, wait *kube.WaitStrategy) { + cmd.Flags().Var( + newWaitValue(kube.HookOnlyStrategy, wait), + "wait", + "if specified, wait until resources are ready (up to --timeout). Values: 'watcher', 'hookOnly', and 'legacy'.", + ) + // Sets the strategy to use the watcher strategy if `--wait` is used without an argument + cmd.Flags().Lookup("wait").NoOptDefVal = string(kube.StatusWatcherStrategy) +} + +type waitValue kube.WaitStrategy + +func newWaitValue(defaultValue kube.WaitStrategy, ws *kube.WaitStrategy) *waitValue { + *ws = defaultValue + return (*waitValue)(ws) +} + +func (ws *waitValue) String() string { + if ws == nil { + return "" + } + return string(*ws) +} + +func (ws *waitValue) Set(s string) error { + switch s { + case string(kube.StatusWatcherStrategy), string(kube.LegacyStrategy), string(kube.HookOnlyStrategy): + *ws = waitValue(s) + return nil + case "true": + slog.Warn("--wait=true is deprecated (boolean value) and can be replaced with --wait=watcher") + *ws = waitValue(kube.StatusWatcherStrategy) + return nil + case "false": + slog.Warn("--wait=false is deprecated (boolean value) and can be replaced with --wait=hookOnly") + *ws = waitValue(kube.HookOnlyStrategy) + return nil + default: + return fmt.Errorf("invalid wait input %q. Valid inputs are %s, %s, and %s", s, kube.StatusWatcherStrategy, kube.HookOnlyStrategy, kube.LegacyStrategy) + } +} + +func (ws *waitValue) Type() string { + return "WaitStrategy" +} + +func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) { + f.StringVar(&c.Version, "version", "", "specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used") + f.BoolVar(&c.Verify, "verify", false, "verify the package before using it") + f.StringVar(&c.Keyring, "keyring", defaultKeyring(), "location of public keys used for verification") + f.StringVar(&c.RepoURL, "repo", "", "chart repository url where to locate the requested chart") + f.StringVar(&c.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&c.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&c.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&c.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.BoolVar(&c.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") + f.BoolVar(&c.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") + f.StringVar(&c.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&c.PassCredentialsAll, "pass-credentials", false, "pass credentials to all domains") +} + +// bindOutputFlag will add the output flag to the given command and bind the +// value to the given format pointer +func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) { + cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o", + fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", "))) + + err := cmd.RegisterFlagCompletionFunc(outputFlag, func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + var formatNames []string + for format, desc := range output.FormatsWithDesc() { + formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc)) + } + + // Sort the results to get a deterministic order for the tests + sort.Strings(formatNames) + return formatNames, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } +} + +type outputValue output.Format + +func newOutputValue(defaultValue output.Format, p *output.Format) *outputValue { + *p = defaultValue + return (*outputValue)(p) +} + +func (o *outputValue) String() string { + // It is much cleaner looking (and technically less allocations) to just + // convert to a string rather than type asserting to the underlying + // output.Format + return string(*o) +} + +func (o *outputValue) Type() string { + return "format" +} + +func (o *outputValue) Set(s string) error { + outfmt, err := output.ParseFormat(s) + if err != nil { + return err + } + *o = outputValue(outfmt) + return nil +} + +func compVersionFlag(chartRef string, _ string) ([]string, cobra.ShellCompDirective) { + chartInfo := strings.Split(chartRef, "/") + if len(chartInfo) != 2 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + repoName := chartInfo[0] + chartName := chartInfo[1] + + path := filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) + + var versions []string + if indexFile, err := repo.LoadIndexFile(path); err == nil { + for _, details := range indexFile.Entries[chartName] { + appVersion := details.AppVersion + appVersionDesc := "" + if appVersion != "" { + appVersionDesc = fmt.Sprintf("App: %s, ", appVersion) + } + created := details.Created.Format("January 2, 2006") + createdDesc := "" + if created != "" { + createdDesc = fmt.Sprintf("Created: %s ", created) + } + deprecated := "" + if details.Deprecated { + deprecated = "(deprecated)" + } + versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Version, appVersionDesc, createdDesc, deprecated)) + } + } + + return versions, cobra.ShellCompDirectiveNoFileComp +} + +// addKlogFlags adds flags from k8s.io/klog +// marks the flags as hidden to avoid polluting the help text +func addKlogFlags(fs *pflag.FlagSet) { + local := flag.NewFlagSet("klog", flag.ExitOnError) + klog.InitFlags(local) + local.VisitAll(func(fl *flag.Flag) { + fl.Name = normalize(fl.Name) + if fs.Lookup(fl.Name) != nil { + return + } + newflag := pflag.PFlagFromGoFlag(fl) + newflag.Hidden = true + fs.AddFlag(newflag) + }) +} + +// normalize replaces underscores with hyphens +func normalize(s string) string { + return strings.ReplaceAll(s, "_", "-") +} diff --git a/pkg/helm/pkg/cmd/helpers.go b/pkg/helm/pkg/cmd/helpers.go new file mode 100644 index 00000000..ce218094 --- /dev/null +++ b/pkg/helm/pkg/cmd/helpers.go @@ -0,0 +1,83 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "log/slog" + "strconv" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/action" +) + +func addDryRunFlag(cmd *cobra.Command) { + // --dry-run options with expected outcome: + // - Not set means no dry run and server is contacted. + // - Set with no value, a value of client, or a value of true and the server is not contacted + // - Set with a value of false, none, or false and the server is contacted + // The true/false part is meant to reflect some legacy behavior while none is equal to "". + f := cmd.Flags() + f.String( + "dry-run", + "none", + `simulates the operation without persisting changes. Must be one of: "none" (default), "client", or "server". '--dry-run=none' executes the operation normally and persists changes (no simulation). '--dry-run=client' simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates the operation on the server, requiring cluster connectivity.`) + f.Lookup("dry-run").NoOptDefVal = "unset" +} + +// Determine the `action.DryRunStrategy` given -dry-run=` flag (or absence of) +// Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted +func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) { + + f := cmd.Flag("dry-run") + v := f.Value.String() + + switch v { + case f.NoOptDefVal: + slog.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`) + return action.DryRunClient, nil + case string(action.DryRunClient): + return action.DryRunClient, nil + case string(action.DryRunServer): + return action.DryRunServer, nil + case string(action.DryRunNone): + if isTemplate { + // Special case hack for `helm template`, which is always a dry run + return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v) + } + return action.DryRunNone, nil + } + + b, err := strconv.ParseBool(v) + if err != nil { + return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "none", "server", or "client"`, v) + } + + if isTemplate && !b { + // Special case for `helm template`, which is always a dry run + return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v) + } + + result := action.DryRunNone + if b { + result = action.DryRunClient + } + slog.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result)) + + return result, nil +} diff --git a/pkg/helm/pkg/cmd/helpers_test.go b/pkg/helm/pkg/cmd/helpers_test.go new file mode 100644 index 00000000..a36e31a9 --- /dev/null +++ b/pkg/helm/pkg/cmd/helpers_test.go @@ -0,0 +1,226 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + shellwords "github.com/mattn/go-shellwords" + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/intern/test" + "github.com/werf/nelm/pkg/helm/pkg/action" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/cli" + kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + "github.com/werf/nelm/pkg/helm/pkg/storage" + "github.com/werf/nelm/pkg/helm/pkg/storage/driver" +) + +func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } + +func init() { + action.Timestamper = testTimestamper +} + +func runTestCmd(t *testing.T, tests []cmdTestCase) { + t.Helper() + for _, tt := range tests { + for i := 0; i <= tt.repeat; i++ { + t.Run(tt.name, func(t *testing.T) { + defer resetEnv()() + + storage := storageFixture() + for _, rel := range tt.rels { + if err := storage.Create(rel); err != nil { + t.Fatal(err) + } + } + t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd) + _, out, err := executeActionCommandC(storage, tt.cmd) + if tt.wantError && err == nil { + t.Errorf("expected error, got success with the following output:\n%s", out) + } + if !tt.wantError && err != nil { + t.Errorf("expected no error, got: '%v'", err) + } + if tt.golden != "" { + test.AssertGoldenString(t, out, tt.golden) + } + }) + } + } +} + +func storageFixture() *storage.Storage { + return storage.Init(driver.NewMemory()) +} + +func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { + return executeActionCommandStdinC(store, nil, cmd) +} + +func executeActionCommandStdinC(store *storage.Storage, in *os.File, cmd string) (*cobra.Command, string, error) { + args, err := shellwords.Parse(cmd) + if err != nil { + return nil, "", err + } + + buf := new(bytes.Buffer) + + actionConfig := &action.Configuration{ + Releases: store, + KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard}, + Capabilities: chartcommon.DefaultCapabilities, + } + + root, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) + if err != nil { + return nil, "", err + } + + root.SetOut(buf) + root.SetErr(buf) + root.SetArgs(args) + + oldStdin := os.Stdin + defer func() { + os.Stdin = oldStdin + }() + + if in != nil { + root.SetIn(in) + os.Stdin = in + } + + if mem, ok := store.Driver.(*driver.Memory); ok { + mem.SetNamespace(settings.Namespace()) + } + c, err := root.ExecuteC() + + result := buf.String() + + return c, result, err +} + +// cmdTestCase describes a test case that works with releases. +type cmdTestCase struct { + name string + cmd string + golden string + wantError bool + // Rels are the available releases at the start of the test. + rels []*release.Release + // Number of repeats (in case a feature was previously flaky and the test checks + // it's now stably producing identical results). 0 means test is run exactly once. + repeat int +} + +func executeActionCommand(cmd string) (*cobra.Command, string, error) { + return executeActionCommandC(storageFixture(), cmd) +} + +func resetEnv() func() { + origEnv := os.Environ() + return func() { + os.Clearenv() + for _, pair := range origEnv { + kv := strings.SplitN(pair, "=", 2) + os.Setenv(kv[0], kv[1]) + } + settings = cli.New() + } +} + +func outputFlagCompletionTest(t *testing.T, cmdName string) { + t.Helper() + releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { + info.LastDeployed = time.Unix(1452902400, 0).UTC() + return []*release.Release{{ + Name: "athos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "porthos", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "aramis", + Namespace: "default", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }, { + Name: "dartagnan", + Namespace: "gascony", + Info: info, + Chart: &chart.Chart{}, + Hooks: hooks, + }} + } + + tests := []cmdTestCase{{ + name: "completion for output flag long and before arg", + cmd: fmt.Sprintf("__complete %s --output ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + }), + }, { + name: "completion for output flag long and after arg", + cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + }), + }, { + name: "completion for output flag short and before arg", + cmd: fmt.Sprintf("__complete %s -o ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + }), + }, { + name: "completion for output flag short and after arg", + cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + }), + }, { + name: "completion for output flag, no filter", + cmd: fmt.Sprintf("__complete %s --output jso", cmdName), + golden: "output/output-comp.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + }), + }} + runTestCmd(t, tests) +} diff --git a/pkg/helm/pkg/cmd/history.go b/pkg/helm/pkg/cmd/history.go new file mode 100644 index 00000000..902a0628 --- /dev/null +++ b/pkg/helm/pkg/cmd/history.go @@ -0,0 +1,270 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "io" + "strconv" + "time" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/action" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + releaseutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" +) + +var historyHelp = ` +History prints historical revisions for a given release. + +A default maximum of 256 revisions will be returned. Setting '--max' +configures the maximum length of the revision list returned. + +The historical release set is printed as a formatted table, e.g: + + $ helm history angry-bird + REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION + 1 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Initial install + 2 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Upgraded successfully + 3 Mon Oct 3 10:15:13 2016 superseded alpine-0.1.0 1.0 Rolled back to 2 + 4 Mon Oct 3 10:15:13 2016 deployed alpine-0.1.0 1.0 Upgraded successfully +` + +func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewHistory(cfg) + var outfmt output.Format + + cmd := &cobra.Command{ + Use: "history RELEASE_NAME", + Long: historyHelp, + Short: "fetch release history", + Aliases: []string{"hist"}, + Args: require.ExactArgs(1), + ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return noMoreArgsComp() + } + return compListReleases(toComplete, args, cfg) + }, + RunE: func(_ *cobra.Command, args []string) error { + history, err := getHistory(client, args[0]) + if err != nil { + return err + } + + return outfmt.Write(out, history) + }, + } + + f := cmd.Flags() + f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history") + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +type releaseInfo struct { + Revision int `json:"revision"` + Updated time.Time `json:"updated,omitzero"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + +// releaseInfoJSON is used for custom JSON marshaling/unmarshaling +type releaseInfoJSON struct { + Revision int `json:"revision"` + Updated *time.Time `json:"updated,omitempty"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It handles empty string time fields by treating them as zero values. +func (r *releaseInfo) UnmarshalJSON(data []byte) error { + // First try to unmarshal into a map to handle empty string time fields + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Replace empty string time fields with nil + if val, ok := raw["updated"]; ok { + if str, ok := val.(string); ok && str == "" { + raw["updated"] = nil + } + } + + // Re-marshal with cleaned data + cleaned, err := json.Marshal(raw) + if err != nil { + return err + } + + // Unmarshal into temporary struct with pointer time field + var tmp releaseInfoJSON + if err := json.Unmarshal(cleaned, &tmp); err != nil { + return err + } + + // Copy values to releaseInfo struct + r.Revision = tmp.Revision + if tmp.Updated != nil { + r.Updated = *tmp.Updated + } + r.Status = tmp.Status + r.Chart = tmp.Chart + r.AppVersion = tmp.AppVersion + r.Description = tmp.Description + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +// It omits zero-value time fields from the JSON output. +func (r releaseInfo) MarshalJSON() ([]byte, error) { + tmp := releaseInfoJSON{ + Revision: r.Revision, + Status: r.Status, + Chart: r.Chart, + AppVersion: r.AppVersion, + Description: r.Description, + } + + if !r.Updated.IsZero() { + tmp.Updated = &r.Updated + } + + return json.Marshal(tmp) +} + +type releaseHistory []releaseInfo + +func (r releaseHistory) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, r) +} + +func (r releaseHistory) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, r) +} + +func (r releaseHistory) WriteTable(out io.Writer) error { + tbl := uitable.New() + tbl.AddRow("REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "DESCRIPTION") + for _, item := range r { + tbl.AddRow(item.Revision, item.Updated.Format(time.ANSIC), item.Status, item.Chart, item.AppVersion, item.Description) + } + return output.EncodeTable(out, tbl) +} + +func getHistory(client *action.History, name string) (releaseHistory, error) { + histi, err := client.Run(name) + if err != nil { + return nil, err + } + hist, err := releaseListToV1List(histi) + if err != nil { + return nil, err + } + + releaseutil.Reverse(hist, releaseutil.SortByRevision) + + var rels []*release.Release + for i := 0; i < min(len(hist), client.Max); i++ { + rels = append(rels, hist[i]) + } + + if len(rels) == 0 { + return releaseHistory{}, nil + } + + releaseHistory := getReleaseHistory(rels) + + return releaseHistory, nil +} + +func getReleaseHistory(rls []*release.Release) (history releaseHistory) { + for i := len(rls) - 1; i >= 0; i-- { + r := rls[i] + c := formatChartName(r.Chart) + s := r.Info.Status.String() + v := r.Version + d := r.Info.Description + a := formatAppVersion(r.Chart) + + rInfo := releaseInfo{ + Revision: v, + Status: s, + Chart: c, + AppVersion: a, + Description: d, + } + if !r.Info.LastDeployed.IsZero() { + rInfo.Updated = r.Info.LastDeployed + + } + history = append(history, rInfo) + } + + return history +} + +func formatChartName(c *chart.Chart) string { + if c == nil || c.Metadata == nil { + // This is an edge case that has happened in prod, though we don't + // know how: https://github.com/helm/helm/issues/1347 + return "MISSING" + } + return fmt.Sprintf("%s-%s", c.Name(), c.Metadata.Version) +} + +func formatAppVersion(c *chart.Chart) string { + if c == nil || c.Metadata == nil { + // This is an edge case that has happened in prod, though we don't + // know how: https://github.com/helm/helm/issues/1347 + return "MISSING" + } + return c.AppVersion() +} + +func compListRevisions(_ string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) { + client := action.NewHistory(cfg) + + var revisions []string + if histi, err := client.Run(releaseName); err == nil { + hist, err := releaseListToV1List(histi) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + for _, version := range hist { + appVersion := fmt.Sprintf("App: %s", version.Chart.Metadata.AppVersion) + chartDesc := fmt.Sprintf("Chart: %s-%s", version.Chart.Metadata.Name, version.Chart.Metadata.Version) + revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(version.Version), appVersion, chartDesc)) + } + return revisions, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveError +} diff --git a/pkg/helm/pkg/cmd/history_test.go b/pkg/helm/pkg/cmd/history_test.go new file mode 100644 index 00000000..27856eb3 --- /dev/null +++ b/pkg/helm/pkg/cmd/history_test.go @@ -0,0 +1,333 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +func TestHistoryCmd(t *testing.T) { + mk := func(name string, vers int, status common.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ + Name: name, + Version: vers, + Status: status, + }) + } + + tests := []cmdTestCase{{ + name: "get history for release", + cmd: "history angry-bird", + rels: []*release.Release{ + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), + mk("angry-bird", 2, common.StatusSuperseded), + mk("angry-bird", 1, common.StatusSuperseded), + }, + golden: "output/history.txt", + }, { + name: "get history with max limit set", + cmd: "history angry-bird --max 2", + rels: []*release.Release{ + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), + }, + golden: "output/history-limit.txt", + }, { + name: "get history with yaml output format", + cmd: "history angry-bird --output yaml", + rels: []*release.Release{ + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), + }, + golden: "output/history.yaml", + }, { + name: "get history with json output format", + cmd: "history angry-bird --output json", + rels: []*release.Release{ + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), + }, + golden: "output/history.json", + }} + runTestCmd(t, tests) +} + +func TestHistoryOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "history") +} + +func revisionFlagCompletionTest(t *testing.T, cmdName string) { + t.Helper() + mk := func(name string, vers int, status common.Status) *release.Release { + return release.Mock(&release.MockReleaseOptions{ + Name: name, + Version: vers, + Status: status, + }) + } + + releases := []*release.Release{ + mk("musketeers", 11, common.StatusDeployed), + mk("musketeers", 10, common.StatusSuperseded), + mk("musketeers", 9, common.StatusSuperseded), + mk("musketeers", 8, common.StatusSuperseded), + } + + tests := []cmdTestCase{{ + name: "completion for revision flag", + cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), + rels: releases, + golden: "output/revision-comp.txt", + }, { + name: "completion for revision flag, no filter", + cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName), + rels: releases, + golden: "output/revision-comp.txt", + }, { + name: "completion for revision flag with too few args", + cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), + rels: releases, + golden: "output/revision-wrong-args-comp.txt", + }, { + name: "completion for revision flag with too many args", + cmd: fmt.Sprintf("__complete %s three musketeers --revision ''", cmdName), + rels: releases, + golden: "output/revision-wrong-args-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestHistoryCompletion(t *testing.T) { + checkReleaseCompletion(t, "history", false) +} + +func TestHistoryFileCompletion(t *testing.T) { + checkFileCompletion(t, "history", false) + checkFileCompletion(t, "history myrelease", false) +} + +func TestReleaseInfoMarshalJSON(t *testing.T) { + updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + info releaseInfo + expected string + }{ + { + name: "all fields populated", + info: releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + }, + expected: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`, + }, + { + name: "without updated time", + info: releaseInfo{ + Revision: 2, + Status: "superseded", + Chart: "mychart-1.0.1", + AppVersion: "1.0.1", + Description: "Upgraded", + }, + expected: `{"revision":2,"status":"superseded","chart":"mychart-1.0.1","app_version":"1.0.1","description":"Upgraded"}`, + }, + { + name: "with zero revision", + info: releaseInfo{ + Revision: 0, + Updated: updated, + Status: "failed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Install failed", + }, + expected: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"failed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install failed"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.info) + require.NoError(t, err) + assert.JSONEq(t, tt.expected, string(data)) + }) + } +} + +func TestReleaseInfoUnmarshalJSON(t *testing.T) { + updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + input string + expected releaseInfo + wantErr bool + }{ + { + name: "all fields populated", + input: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`, + expected: releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + }, + }, + { + name: "empty string updated field", + input: `{"revision":2,"updated":"","status":"superseded","chart":"mychart-1.0.1","app_version":"1.0.1","description":"Upgraded"}`, + expected: releaseInfo{ + Revision: 2, + Status: "superseded", + Chart: "mychart-1.0.1", + AppVersion: "1.0.1", + Description: "Upgraded", + }, + }, + { + name: "missing updated field", + input: `{"revision":3,"status":"deployed","chart":"mychart-1.0.2","app_version":"1.0.2","description":"Upgraded"}`, + expected: releaseInfo{ + Revision: 3, + Status: "deployed", + Chart: "mychart-1.0.2", + AppVersion: "1.0.2", + Description: "Upgraded", + }, + }, + { + name: "null updated field", + input: `{"revision":4,"updated":null,"status":"failed","chart":"mychart-1.0.3","app_version":"1.0.3","description":"Failed"}`, + expected: releaseInfo{ + Revision: 4, + Status: "failed", + Chart: "mychart-1.0.3", + AppVersion: "1.0.3", + Description: "Failed", + }, + }, + { + name: "invalid time format", + input: `{"revision":5,"updated":"invalid-time","status":"deployed","chart":"mychart-1.0.4","app_version":"1.0.4","description":"Test"}`, + wantErr: true, + }, + { + name: "zero revision", + input: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"pending-install","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Installing"}`, + expected: releaseInfo{ + Revision: 0, + Updated: updated, + Status: "pending-install", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Installing", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var info releaseInfo + err := json.Unmarshal([]byte(tt.input), &info) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.Revision, info.Revision) + assert.Equal(t, tt.expected.Updated.Unix(), info.Updated.Unix()) + assert.Equal(t, tt.expected.Status, info.Status) + assert.Equal(t, tt.expected.Chart, info.Chart) + assert.Equal(t, tt.expected.AppVersion, info.AppVersion) + assert.Equal(t, tt.expected.Description, info.Description) + }) + } +} + +func TestReleaseInfoRoundTrip(t *testing.T) { + updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + + original := releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + } + + data, err := json.Marshal(&original) + require.NoError(t, err) + + var decoded releaseInfo + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.Revision, decoded.Revision) + assert.Equal(t, original.Updated.Unix(), decoded.Updated.Unix()) + assert.Equal(t, original.Status, decoded.Status) + assert.Equal(t, original.Chart, decoded.Chart) + assert.Equal(t, original.AppVersion, decoded.AppVersion) + assert.Equal(t, original.Description, decoded.Description) +} + +func TestReleaseInfoEmptyStringRoundTrip(t *testing.T) { + // This test specifically verifies that empty string time fields + // are handled correctly during parsing + input := `{"revision":1,"updated":"","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Test"}` + + var info releaseInfo + err := json.Unmarshal([]byte(input), &info) + require.NoError(t, err) + + // Verify time field is zero value + assert.True(t, info.Updated.IsZero()) + assert.Equal(t, 1, info.Revision) + assert.Equal(t, "deployed", info.Status) + + // Marshal back and verify empty time field is omitted + data, err := json.Marshal(&info) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Zero time value should be omitted + assert.NotContains(t, result, "updated") + assert.Equal(t, float64(1), result["revision"]) + assert.Equal(t, "deployed", result["status"]) + assert.Equal(t, "mychart-1.0.0", result["chart"]) +} diff --git a/pkg/helm/pkg/cmd/list.go b/pkg/helm/pkg/cmd/list.go new file mode 100644 index 00000000..dd75f631 --- /dev/null +++ b/pkg/helm/pkg/cmd/list.go @@ -0,0 +1,289 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + "os" + "slices" + "strconv" + + "github.com/gosuri/uitable" + "github.com/spf13/cobra" + + coloroutput "github.com/werf/nelm/pkg/helm/intern/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +var listHelp = ` +This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). + +By default, it lists all releases in any status. Individual status filters like '--deployed', '--failed', +'--pending', '--uninstalled', '--superseded', and '--uninstalling' can be used +to show only releases in specific states. Such flags can be combined: +'--deployed --failed'. + +By default, items are sorted alphabetically. Use the '-d' flag to sort by +release date. + +If the --filter flag is provided, it will be treated as a filter. Filters are +regular expressions (Perl compatible) that are applied to the list of releases. +Only items that match the filter will be returned. + + $ helm list --filter 'ara[a-z]+' + NAME UPDATED CHART + maudlin-arachnid 2020-06-18 14:17:46.125134977 +0000 UTC alpine-0.1.0 + +If no results are found, 'helm list' will exit 0, but with no output (or in +the case of no '-q' flag, only headers). + +By default, up to 256 items may be returned. To limit this, use the '--max' flag. +Setting '--max' to 0 will not return all results. Rather, it will return the +server's default, which may be much higher than 256. Pairing the '--max' +flag with the '--offset' flag allows you to page through results. +` + +func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewList(cfg) + var outfmt output.Format + + cmd := &cobra.Command{ + Use: "list", + Short: "list releases", + Long: listHelp, + Aliases: []string{"ls"}, + Args: require.NoArgs, + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(cmd *cobra.Command, _ []string) error { + if client.AllNamespaces { + if err := cfg.Init(settings.RESTClientGetter(), "", os.Getenv("HELM_DRIVER")); err != nil { + return err + } + } + client.SetStateMask() + + resultsi, err := client.Run() + if err != nil { + return err + } + results, err := releaseListToV1List(resultsi) + if err != nil { + return err + } + + if client.Short { + names := make([]string, 0, len(results)) + for _, res := range results { + names = append(names, res.Name) + } + + outputFlag := cmd.Flag("output") + + switch outputFlag.Value.String() { + case "json": + output.EncodeJSON(out, names) + return nil + case "yaml": + output.EncodeYAML(out, names) + return nil + case "table": + for _, res := range results { + fmt.Fprintln(out, res.Name) + } + return nil + } + } + + return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders, settings.ShouldDisableColor())) + }, + } + + f := cmd.Flags() + f.BoolVarP(&client.Short, "short", "q", false, "output short (quiet) listing format") + f.BoolVarP(&client.NoHeaders, "no-headers", "", false, "don't print headers when using the default output format") + f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`) + f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") + f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") + f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)") + f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") + f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") + f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases") + f.BoolVar(&client.Failed, "failed", false, "show failed releases") + f.BoolVar(&client.Pending, "pending", false, "show pending releases") + f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces") + f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") + f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value") + f.StringVarP(&client.Filter, "filter", "f", "", "a regular expression (Perl compatible). Any releases that match the expression will be included in the results") + f.StringVarP(&client.Selector, "selector", "l", "", "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Works only for secret(default) and configmap storage backends.") + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +type releaseElement struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Revision string `json:"revision"` + Updated string `json:"updated"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` +} + +type releaseListWriter struct { + releases []releaseElement + noHeaders bool + noColor bool +} + +func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool, noColor bool) *releaseListWriter { + // Initialize the array so no results returns an empty array instead of null + elements := make([]releaseElement, 0, len(releases)) + for _, r := range releases { + element := releaseElement{ + Name: r.Name, + Namespace: r.Namespace, + Revision: strconv.Itoa(r.Version), + Status: r.Info.Status.String(), + Chart: formatChartName(r.Chart), + AppVersion: formatAppVersion(r.Chart), + } + + t := "-" + if tspb := r.Info.LastDeployed; !tspb.IsZero() { + if timeFormat != "" { + t = tspb.Format(timeFormat) + } else { + t = tspb.String() + } + } + element.Updated = t + + elements = append(elements, element) + } + return &releaseListWriter{elements, noHeaders, noColor} +} + +func (w *releaseListWriter) WriteTable(out io.Writer) error { + table := uitable.New() + if !w.noHeaders { + table.AddRow( + coloroutput.ColorizeHeader("NAME", w.noColor), + coloroutput.ColorizeHeader("NAMESPACE", w.noColor), + coloroutput.ColorizeHeader("REVISION", w.noColor), + coloroutput.ColorizeHeader("UPDATED", w.noColor), + coloroutput.ColorizeHeader("STATUS", w.noColor), + coloroutput.ColorizeHeader("CHART", w.noColor), + coloroutput.ColorizeHeader("APP VERSION", w.noColor), + ) + } + for _, r := range w.releases { + // Parse the status string back to a release.Status to use color + var status common.Status + switch r.Status { + case "deployed": + status = common.StatusDeployed + case "failed": + status = common.StatusFailed + case "pending-install": + status = common.StatusPendingInstall + case "pending-upgrade": + status = common.StatusPendingUpgrade + case "pending-rollback": + status = common.StatusPendingRollback + case "uninstalling": + status = common.StatusUninstalling + case "uninstalled": + status = common.StatusUninstalled + case "superseded": + status = common.StatusSuperseded + case "unknown": + status = common.StatusUnknown + default: + status = common.Status(r.Status) + } + table.AddRow(r.Name, coloroutput.ColorizeNamespace(r.Namespace, w.noColor), r.Revision, r.Updated, coloroutput.ColorizeStatus(status, w.noColor), r.Chart, r.AppVersion) + } + return output.EncodeTable(out, table) +} + +func (w *releaseListWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, w.releases) +} + +func (w *releaseListWriter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, w.releases) +} + +// Returns all releases from 'releases', except those with names matching 'ignoredReleases' +func filterReleases(releases []*release.Release, ignoredReleaseNames []string) []*release.Release { + // if ignoredReleaseNames is nil, just return releases + if ignoredReleaseNames == nil { + return releases + } + + var filteredReleases []*release.Release + for _, rel := range releases { + found := slices.Contains(ignoredReleaseNames, rel.Name) + if !found { + filteredReleases = append(filteredReleases, rel) + } + } + + return filteredReleases +} + +// Provide dynamic auto-completion for release names +func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *action.Configuration) ([]string, cobra.ShellCompDirective) { + cobra.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete), settings.Debug) + + client := action.NewList(cfg) + client.All = true + client.Limit = 0 + // Do not filter so as to get the entire list of releases. + // This will allow zsh and fish to match completion choices + // on other criteria then prefix. For example: + // helm status ingress + // can match + // helm status nginx-ingress + // + // client.Filter = fmt.Sprintf("^%s", toComplete) + + client.SetStateMask() + releasesi, err := client.Run() + if err != nil { + return nil, cobra.ShellCompDirectiveDefault + } + releases, err := releaseListToV1List(releasesi) + if err != nil { + return nil, cobra.ShellCompDirectiveDefault + } + + var choices []string + filteredReleases := filterReleases(releases, ignoredReleaseNames) + for _, rel := range filteredReleases { + choices = append(choices, + fmt.Sprintf("%s\t%s-%s -> %s", rel.Name, rel.Chart.Metadata.Name, rel.Chart.Metadata.Version, rel.Info.Status.String())) + } + + return choices, cobra.ShellCompDirectiveNoFileComp +} diff --git a/pkg/helm/pkg/cmd/package.go b/pkg/helm/pkg/cmd/package.go new file mode 100644 index 00000000..2fdfe892 --- /dev/null +++ b/pkg/helm/pkg/cmd/package.go @@ -0,0 +1,142 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cli/values" + "github.com/werf/nelm/pkg/helm/pkg/downloader" + "github.com/werf/nelm/pkg/helm/pkg/getter" + "github.com/werf/nelm/pkg/ts" +) + +const packageDesc = ` +This command packages a chart into a versioned chart archive file. If a path +is given, this will look at that path for a chart (which must contain a +Chart.yaml file) and then package that directory. + +Versioned chart archives are used by Helm package repositories. + +To sign a chart, use the '--sign' flag. In most cases, you should also +provide '--keyring path/to/secret/keys' and '--key keyname'. + + $ helm package --sign ./mychart --key mykey --keyring ~/.gnupg/secring.gpg + +If '--keyring' is not specified, Helm usually defaults to the public keyring +unless your environment is otherwise configured. +` + +func newPackageCmd(out io.Writer) *cobra.Command { + client := action.NewPackage() + valueOpts := &values.Options{} + + cmd := &cobra.Command{ + Use: "package [CHART_PATH] [...]", + Short: "package a chart directory into a chart archive", + Long: packageDesc, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("need at least one argument, the path to the chart") + } + if client.Sign { + if client.Key == "" { + return errors.New("--key is required for signing a package") + } + if client.Keyring == "" { + return errors.New("--keyring is required for signing a package") + } + } + + client.TypeScriptOps = ts.GetTSOptionsFromContext(cmd.Context()) + client.RepositoryConfig = settings.RepositoryConfig + client.RepositoryCache = settings.RepositoryCache + p := getter.All(settings) + vals, err := valueOpts.MergeValues(context.Background(), p) + if err != nil { + return err + } + + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + + for i := range args { + path, err := filepath.Abs(args[i]) + if err != nil { + return err + } + if _, err := os.Stat(args[i]); err != nil { + return err + } + + if client.DependencyUpdate { + downloadManager := &downloader.Manager{ + Out: io.Discard, + ChartPath: path, + Keyring: client.Keyring, + Getters: p, + Debug: settings.Debug, + RegistryClient: registryClient, + RepositoryConfig: settings.RepositoryConfig, + RepositoryCache: settings.RepositoryCache, + ContentCache: settings.ContentCache, + } + + if err := downloadManager.Update(context.Background()); err != nil { + return err + } + } + p, err := client.Run(path, vals) + if err != nil { + return err + } + fmt.Fprintf(out, "Successfully packaged chart and saved it to: %s\n", p) + } + return nil + }, + } + + f := cmd.Flags() + f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package") + f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true") + f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring") + f.StringVar(&client.PassphraseFile, "passphrase-file", "", `location of a file which contains the passphrase for the signing key. Use "-" in order to read from stdin.`) + f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version") + f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") + f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") + f.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`) + f.StringVar(&client.Username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&client.Password, "password", "", "chart repository password where to locate the requested chart") + f.StringVar(&client.CertFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") + f.StringVar(&client.KeyFile, "key-file", "", "identify HTTPS client using this SSL key file") + f.BoolVar(&client.InsecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart download") + f.BoolVar(&client.PlainHTTP, "plain-http", false, "use insecure HTTP connections for the chart download") + f.StringVar(&client.CaFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + + return cmd +} diff --git a/pkg/helm/cmd/helm/package_test.go b/pkg/helm/pkg/cmd/package_test.go similarity index 94% rename from pkg/helm/cmd/helm/package_test.go rename to pkg/helm/pkg/cmd/package_test.go index 22631399..d3f1d99a 100644 --- a/pkg/helm/cmd/helm/package_test.go +++ b/pkg/helm/pkg/cmd/package_test.go @@ -13,9 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "context" "fmt" "os" "path/filepath" @@ -23,8 +24,9 @@ import ( "strings" "testing" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + "github.com/werf/nelm/pkg/helm/intern/test/ensure" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" ) func TestPackage(t *testing.T) { @@ -110,10 +112,10 @@ func TestPackage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cachePath := t.TempDir() - defer testChdir(t, cachePath)() + t.Chdir(t.TempDir()) + ensure.HelmHome(t) - if err := os.MkdirAll("toot", 0777); err != nil { + if err := os.MkdirAll("toot", 0o777); err != nil { t.Fatal(err) } @@ -182,7 +184,7 @@ func TestSetAppVersion(t *testing.T) { } else if fi.Size() == 0 { t.Errorf("file %q has zero bytes.", chartPath) } - ch, err = loader.Load(chartPath) + ch, err = loader.Load(context.Background(), chartPath) if err != nil { t.Fatalf("unexpected error loading packaged chart: %v", err) } diff --git a/pkg/helm/pkg/cmd/printer.go b/pkg/helm/pkg/cmd/printer.go new file mode 100644 index 00000000..30238f5b --- /dev/null +++ b/pkg/helm/pkg/cmd/printer.go @@ -0,0 +1,30 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "io" + "text/template" +) + +func tpl(t string, vals map[string]interface{}, out io.Writer) error { + tt, err := template.New("_").Parse(t) + if err != nil { + return err + } + return tt.Execute(out, vals) +} diff --git a/pkg/helm/cmd/helm/pull.go b/pkg/helm/pkg/cmd/pull.go similarity index 84% rename from pkg/helm/cmd/helm/pull.go rename to pkg/helm/pkg/cmd/pull.go index f5abcde1..fa42b0a6 100644 --- a/pkg/helm/cmd/helm/pull.go +++ b/pkg/helm/pkg/cmd/pull.go @@ -14,17 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" "io" "log" + "log/slog" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" ) const pullDesc = ` @@ -43,7 +44,7 @@ result in an error, and the chart will not be saved locally. ` func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { - client := action.NewPullWithOpts(action.WithConfig(cfg)) + client := action.NewPull(action.WithConfig(cfg)) cmd := &cobra.Command{ Use: "pull [chart URL | repo/chartname] [...]", @@ -51,27 +52,27 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Aliases: []string{"fetch"}, Long: pullDesc, Args: require.MinimumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp } return compListCharts(toComplete, false) }, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { client.Settings = settings if client.Version == "" && client.Devel { - debug("setting version to >0.0.0-0") + slog.Debug("setting version to >0.0.0-0") client.Version = ">0.0.0-0" } registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, - client.InsecureSkipTLSverify, client.PlainHTTP) + client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) if err != nil { return fmt.Errorf("missing registry client: %w", err) } client.SetRegistryClient(registryClient) - for i := 0; i < len(args); i++ { + for i := range args { output, err := client.Run(args[i]) if err != nil { return err @@ -90,7 +91,7 @@ func newPullCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVarP(&client.DestDir, "destination", "d", ".", "location to write the chart. If this and untardir are specified, untardir is appended to this") addChartPathOptionsFlags(f, &client.ChartPathOptions) - err := cmd.RegisterFlagCompletionFunc("version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + err := cmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 1 { return nil, cobra.ShellCompDirectiveNoFileComp } diff --git a/pkg/helm/pkg/cmd/pull_test.go b/pkg/helm/pkg/cmd/pull_test.go new file mode 100644 index 00000000..d60b5660 --- /dev/null +++ b/pkg/helm/pkg/cmd/pull_test.go @@ -0,0 +1,560 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" +) + +func TestPullCmd(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + ) + defer srv.Stop() + + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + if err != nil { + t.Fatal(err) + } + ociSrv.Run(t) + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + + "Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" + + "Chart Hash Verified: " + + // all flags will get "-d outdir" appended. + tests := []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + failExpect string + expectFile string + expectDir bool + expectVerify bool + expectSha string + }{ + { + name: "Basic chart fetch", + args: "test/signtest", + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Chart fetch with version", + args: "test/signtest --version=0.1.0", + expectFile: "./signtest-0.1.0.tgz", + }, + { + name: "Fail chart fetch with non-existent version", + args: "test/signtest --version=99.1.0", + wantError: true, + failExpect: "no such chart", + }, + { + name: "Fail fetching non-existent chart", + args: "test/nosuchthing", + failExpect: "Failed to fetch", + wantError: true, + }, + { + name: "Fetch and verify", + args: "test/signtest --verify --keyring testdata/helm-test-key.pub", + expectFile: "./signtest-0.1.0.tgz", + expectVerify: true, + expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + }, + { + name: "Fetch and fail verify", + args: "test/reqtest --verify --keyring testdata/helm-test-key.pub", + failExpect: "Failed to fetch provenance", + wantError: true, + }, + { + name: "Fetch and untar", + args: "test/signtest --untar --untardir signtest", + expectFile: "./signtest", + expectDir: true, + }, + { + name: "Fetch untar when file with same name existed", + args: "test/test1 --untar --untardir test1", + existFile: "test1/test1", + wantError: true, + wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test1", "test1")), + }, + { + name: "Fetch untar when dir with same name existed", + args: "test/test --untar --untardir test2", + existDir: "test2/test", + wantError: true, + wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test2", "test")), + }, + { + name: "Fetch, verify, untar", + args: "test/signtest --verify --keyring=testdata/helm-test-key.pub --untar --untardir signtest2", + expectFile: "./signtest2", + expectDir: true, + expectVerify: true, + expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + }, + { + name: "Chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --repo " + srv.URL(), + }, + { + name: "Fail fetching non-existent chart on repo URL", + args: "someChart --repo " + srv.URL(), + failExpect: "Failed to fetch chart", + wantError: true, + }, + { + name: "Specific version chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --version=0.1.0 --repo " + srv.URL(), + }, + { + name: "Specific version chart fetch using repo URL", + args: "signtest --version=0.2.0 --repo " + srv.URL(), + failExpect: "Failed to fetch chart version", + wantError: true, + }, + { + name: "Chart fetch using repo URL with untardir", + args: "signtest --version=0.1.0 --untar --untardir repo-url-test --repo " + srv.URL(), + expectFile: "./signtest", + expectDir: true, + }, + { + name: "Chart fetch using repo URL with untardir and previous pull", + args: "signtest --version=0.1.0 --untar --untardir repo-url-test --repo " + srv.URL(), + failExpect: "failed to untar", + wantError: true, + }, + { + name: "Fetch OCI Chart", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "Fetch OCI Chart with untar", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart", + expectDir: true, + }, + { + name: "Fetch OCI Chart with untar and untardir", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2", ociSrv.RegistryURL), + expectFile: "./ocitest2", + expectDir: true, + }, + { + name: "OCI Fetch untar when dir with same name existed", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart --version 0.1.0 --untar --untardir ocitest2", ociSrv.RegistryURL), + existDir: "ocitest2/oci-dependent-chart", + wantError: true, + wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "ocitest2", "oci-dependent-chart")), + }, + { + name: "Fail fetching non-existent OCI chart", + args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing --version 0.1.0", ociSrv.RegistryURL), + failExpect: "Failed to fetch", + wantError: true, + }, + { + name: "Fail fetching OCI chart without version specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/nosuchthing", ociSrv.RegistryURL), + wantError: true, + }, + { + name: "Fetching OCI chart without version option specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "Fetching OCI chart with version specified", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0 --version 0.1.0", ociSrv.RegistryURL), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "Fail fetching OCI chart with version mismatch", + args: fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.2.0 --version 0.1.0", ociSrv.RegistryURL), + wantErrorMsg: "chart reference and version mismatch: 0.1.0 is not 0.2.0", + wantError: true, + }, + } + + contentCache := t.TempDir() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Root() + cmd := fmt.Sprintf("fetch %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s --content-cache %s --plain-http", + tt.args, + outdir, + filepath.Join(outdir, "repositories.yaml"), + outdir, + filepath.Join(outdir, "config.json"), + contentCache, + ) + // Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182 + if tt.existFile != "" { + file := filepath.Join(outdir, tt.existFile) + if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { + t.Fatal(err) + } + _, err := os.Create(file) + if err != nil { + t.Fatal(err) + } + } + if tt.existDir != "" { + file := filepath.Join(outdir, tt.existDir) + err := os.MkdirAll(file, 0755) + if err != nil { + t.Fatal(err) + } + } + _, out, err := executeActionCommand(cmd) + if err != nil { + if tt.wantError { + if tt.wantErrorMsg != "" && tt.wantErrorMsg != err.Error() { + t.Fatalf("Actual error '%s', not equal to expected error '%s'", err, tt.wantErrorMsg) + } + return + } + t.Fatalf("%q reported error: %s", tt.name, err) + } + + if tt.expectVerify { + outString := helmTestKeyOut + tt.expectSha + "\n" + if out != outString { + t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out) + } + + } + + ef := filepath.Join(outdir, tt.expectFile) + fi, err := os.Stat(ef) + if err != nil { + t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) + } + if fi.IsDir() != tt.expectDir { + t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) + } + }) + } +} + +// runPullTests is a helper function to run pull command tests with common logic +func runPullTests(t *testing.T, tests []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + expectFile string + expectDir bool +}, outdir string, additionalFlags string) { + t.Helper() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := fmt.Sprintf("pull %s -d '%s' --repository-config %s --repository-cache %s --registry-config %s %s", + tt.args, + outdir, + filepath.Join(outdir, "repositories.yaml"), + outdir, + filepath.Join(outdir, "config.json"), + additionalFlags, + ) + // Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182 + if tt.existFile != "" { + file := filepath.Join(outdir, tt.existFile) + _, err := os.Create(file) + if err != nil { + t.Fatal(err) + } + } + if tt.existDir != "" { + file := filepath.Join(outdir, tt.existDir) + err := os.MkdirAll(file, 0755) + if err != nil { + t.Fatal(err) + } + } + _, _, err := executeActionCommand(cmd) + if tt.wantError && err == nil { + t.Fatalf("%q: expected error but got none", tt.name) + } + if err != nil { + if tt.wantError { + if tt.wantErrorMsg != "" && tt.wantErrorMsg != err.Error() { + t.Fatalf("Actual error '%s', not equal to expected error '%s'", err, tt.wantErrorMsg) + } + return + } + t.Fatalf("%q reported error: %s", tt.name, err) + } + + ef := filepath.Join(outdir, tt.expectFile) + fi, err := os.Stat(ef) + if err != nil { + t.Errorf("%q: expected a file at %s. %s", tt.name, ef, err) + } + if fi.IsDir() != tt.expectDir { + t.Errorf("%q: expected directory=%t, but it's not.", tt.name, tt.expectDir) + } + }) + } +} + +// buildOCIURL is a helper function to build OCI URLs with credentials +func buildOCIURL(registryURL, chartName, version, username, password string) string { + baseURL := fmt.Sprintf("oci://%s/u/ocitestuser/%s", registryURL, chartName) + if version != "" { + baseURL += fmt.Sprintf(" --version %s", version) + } + if username != "" && password != "" { + baseURL += fmt.Sprintf(" --username %s --password %s", username, password) + } + return baseURL +} + +func TestPullWithCredentialsCmd(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + repotest.WithMiddleware(repotest.BasicAuthMiddleware(t)), + ) + defer srv.Stop() + + srv2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.FileServer(http.Dir(srv.Root())).ServeHTTP(w, r) + })) + defer srv2.Close() + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + // all flags will get "-d outdir" appended. + tests := []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + expectFile string + expectDir bool + }{ + { + name: "Chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --repo " + srv.URL() + " --username username --password password", + }, + { + name: "Fail fetching non-existent chart on repo URL", + args: "someChart --repo " + srv.URL() + " --username username --password password", + wantError: true, + }, + { + name: "Specific version chart fetch using repo URL", + expectFile: "./signtest-0.1.0.tgz", + args: "signtest --version=0.1.0 --repo " + srv.URL() + " --username username --password password", + }, + { + name: "Specific version chart fetch using repo URL", + args: "signtest --version=0.2.0 --repo " + srv.URL() + " --username username --password password", + wantError: true, + }, + { + name: "Chart located on different domain with credentials passed", + args: "reqtest --repo " + srv2.URL + " --username username --password password --pass-credentials", + expectFile: "./reqtest-0.1.0.tgz", + }, + } + + runPullTests(t, tests, srv.Root(), "") +} + +func TestPullVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for pull version flag", + cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for pull version flag, no filter", + cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for pull version flag too few args", + cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for pull version flag too many args", + cmd: fmt.Sprintf("%s __complete pull testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for pull version flag invalid chart", + cmd: fmt.Sprintf("%s __complete pull invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestPullWithCredentialsCmdOCIRegistry(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + ) + defer srv.Stop() + + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + if err != nil { + t.Fatal(err) + } + ociSrv.Run(t) + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + // all flags will get "-d outdir" appended. + tests := []struct { + name string + args string + existFile string + existDir string + wantError bool + wantErrorMsg string + expectFile string + expectDir bool + }{ + { + name: "OCI Chart fetch with credentials", + args: buildOCIURL(ociSrv.RegistryURL, "oci-dependent-chart", "0.1.0", ociSrv.TestUsername, ociSrv.TestPassword), + expectFile: "./oci-dependent-chart-0.1.0.tgz", + }, + { + name: "OCI Chart fetch with credentials and untar", + args: buildOCIURL(ociSrv.RegistryURL, "oci-dependent-chart", "0.1.0", ociSrv.TestUsername, ociSrv.TestPassword) + " --untar", + expectFile: "./oci-dependent-chart", + expectDir: true, + }, + { + name: "OCI Chart fetch with credentials and untardir", + args: buildOCIURL(ociSrv.RegistryURL, "oci-dependent-chart", "0.1.0", ociSrv.TestUsername, ociSrv.TestPassword) + " --untar --untardir ocitest-credentials", + expectFile: "./ocitest-credentials", + expectDir: true, + }, + { + name: "Fail fetching OCI chart with wrong credentials", + args: buildOCIURL(ociSrv.RegistryURL, "oci-dependent-chart", "0.1.0", "wronguser", "wrongpass"), + wantError: true, + }, + { + name: "Fail fetching non-existent OCI chart with credentials", + args: buildOCIURL(ociSrv.RegistryURL, "nosuchthing", "0.1.0", ociSrv.TestUsername, ociSrv.TestPassword), + wantError: true, + }, + { + name: "Fail fetching OCI chart without version specified", + args: buildOCIURL(ociSrv.RegistryURL, "nosuchthing", "", ociSrv.TestUsername, ociSrv.TestPassword), + wantError: true, + }, + } + + runPullTests(t, tests, srv.Root(), "--plain-http") +} + +func TestPullFileCompletion(t *testing.T) { + checkFileCompletion(t, "pull", false) + checkFileCompletion(t, "pull repo/chart", false) +} + +// TestPullOCIWithTagAndDigest tests pulling an OCI chart with both tag and digest specified. +// This is a regression test for https://github.com/helm/helm/issues/31600 +func TestPullOCIWithTagAndDigest(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + ) + defer srv.Stop() + + ociSrv, err := repotest.NewOCIServer(t, srv.Root()) + if err != nil { + t.Fatal(err) + } + result := ociSrv.RunWithReturn(t) + + contentCache := t.TempDir() + outdir := t.TempDir() + + // Test: pull with tag and digest (the fixed bug from issue #31600) + // Previously this failed with "encoding/hex: invalid byte: U+0073 's'" + ref := fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart:0.1.0@%s", + ociSrv.RegistryURL, result.PushedChart.Manifest.Digest) + + cmd := fmt.Sprintf("pull %s -d '%s' --registry-config %s --content-cache %s --plain-http", + ref, + outdir, + filepath.Join(srv.Root(), "config.json"), + contentCache, + ) + + _, _, err = executeActionCommand(cmd) + if err != nil { + t.Fatalf("pull with tag+digest failed: %v", err) + } + + // Verify the file was downloaded + // When digest is present, the filename uses the digest format (e.g. chart@sha256-hex.tgz) + expectedFile := filepath.Join(outdir, "oci-dependent-chart-0.1.0.tgz") + if _, err := os.Stat(expectedFile); err != nil { + // Try the digest-based filename; parse algorithm:hex to avoid fixed-offset assumptions + algorithm, digestPart, ok := strings.Cut(result.PushedChart.Manifest.Digest, ":") + if !ok { + t.Fatalf("digest must be in algorithm:hex format, got %q", result.PushedChart.Manifest.Digest) + } + expectedFile = filepath.Join(outdir, fmt.Sprintf("oci-dependent-chart@%s-%s.tgz", algorithm, digestPart)) + if _, err := os.Stat(expectedFile); err != nil { + t.Errorf("expected chart file not found: %v", err) + } + } +} diff --git a/pkg/helm/cmd/helm/push.go b/pkg/helm/pkg/cmd/push.go similarity index 75% rename from pkg/helm/cmd/helm/push.go rename to pkg/helm/pkg/cmd/push.go index 8a15375a..a142d794 100644 --- a/pkg/helm/cmd/helm/push.go +++ b/pkg/helm/pkg/cmd/push.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" @@ -22,10 +22,9 @@ import ( "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" "github.com/werf/nelm/pkg/helm/pkg/pusher" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) const pushDesc = ` @@ -39,8 +38,10 @@ type registryPushOptions struct { certFile string keyFile string caFile string - insecureSkipTLSverify bool + insecureSkipTLSVerify bool plainHTTP bool + password string + username string } func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -51,7 +52,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { Short: "push a chart to remote", Long: pushDesc, Args: require.MinimumNArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { // Do file completion for the chart file to push return nil, cobra.ShellCompDirectiveDefault @@ -66,10 +67,13 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace } - return nil, cobra.ShellCompDirectiveNoFileComp + return noMoreArgsComp() }, - RunE: func(cmd *cobra.Command, args []string) error { - registryClient, err := newRegistryClient(o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSverify, o.plainHTTP) + RunE: func(_ *cobra.Command, args []string) error { + registryClient, err := newRegistryClient( + o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSVerify, o.plainHTTP, o.username, o.password, + ) + if err != nil { return fmt.Errorf("missing registry client: %w", err) } @@ -78,18 +82,11 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { remote := args[1] client := action.NewPushWithOpts(action.WithPushConfig(cfg), action.WithTLSClientConfig(o.certFile, o.keyFile, o.caFile), - action.WithInsecureSkipTLSVerify(o.insecureSkipTLSverify), + action.WithInsecureSkipTLSVerify(o.insecureSkipTLSVerify), action.WithPlainHTTP(o.plainHTTP), action.WithPushOptWriter(out)) client.Settings = settings - - opts := helmopts.HelmOptions{ - ChartLoadOpts: helmopts.ChartLoadOptions{ - NoSecrets: true, - }, - } - - output, err := client.Run(chartRef, remote, opts) + output, err := client.Run(chartRef, remote) if err != nil { return err } @@ -102,8 +99,10 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file") f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file") f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload") + f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the chart upload") f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload") + f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart") + f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart") return cmd } diff --git a/pkg/helm/pkg/cmd/push_test.go b/pkg/helm/pkg/cmd/push_test.go new file mode 100644 index 00000000..80d08b48 --- /dev/null +++ b/pkg/helm/pkg/cmd/push_test.go @@ -0,0 +1,27 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestPushFileCompletion(t *testing.T) { + checkFileCompletion(t, "push", true) + checkFileCompletion(t, "push package.tgz", false) + checkFileCompletion(t, "push package.tgz oci://localhost:5000", false) +} diff --git a/pkg/helm/pkg/cmd/registry.go b/pkg/helm/pkg/cmd/registry.go new file mode 100644 index 00000000..bcee4293 --- /dev/null +++ b/pkg/helm/pkg/cmd/registry.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "io" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/action" +) + +const registryHelp = ` +This command consists of multiple subcommands to interact with registries. +` + +func newRegistryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "registry", + Short: "login to or logout from a registry", + Long: registryHelp, + } + cmd.AddCommand( + newRegistryLoginCmd(cfg, out), + newRegistryLogoutCmd(cfg, out), + ) + return cmd +} diff --git a/pkg/helm/cmd/helm/registry_login.go b/pkg/helm/pkg/cmd/registry_login.go similarity index 87% rename from pkg/helm/cmd/helm/registry_login.go rename to pkg/helm/pkg/cmd/registry_login.go index a3c3d8ae..32f1deba 100644 --- a/pkg/helm/cmd/helm/registry_login.go +++ b/pkg/helm/pkg/cmd/registry_login.go @@ -14,25 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "bufio" "errors" "fmt" "io" + "log/slog" "os" "strings" "github.com/moby/term" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" ) const registryLoginDesc = ` Authenticate to a remote registry. + +For example for Github Container Registry: + + echo "$GITHUB_TOKEN" | helm registry login ghcr.io -u $GITHUB_USER --password-stdin ` type registryLoginOptions struct { @@ -43,6 +48,7 @@ type registryLoginOptions struct { keyFile string caFile string insecure bool + plainHTTP bool } func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { @@ -53,8 +59,8 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman Short: "login to a registry", Long: registryLoginDesc, Args: require.MinimumNArgs(1), - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: cobra.NoFileCompletions, + RunE: func(_ *cobra.Command, args []string) error { hostname := args[0] username, password, err := getUsernamePassword(o.username, o.password, o.passwordFromStdinOpt) @@ -66,7 +72,8 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman action.WithCertFile(o.certFile), action.WithKeyFile(o.keyFile), action.WithCAFile(o.caFile), - action.WithInsecure(o.insecure)) + action.WithInsecure(o.insecure), + action.WithPlainHTTPLogin(o.plainHTTP)) }, } @@ -78,6 +85,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman f.StringVar(&o.certFile, "cert-file", "", "identify registry client using this SSL certificate file") f.StringVar(&o.keyFile, "key-file", "", "identify registry client using this SSL key file") f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") + f.BoolVar(&o.plainHTTP, "plain-http", false, "use insecure HTTP connections for the chart upload") return cmd } @@ -119,7 +127,7 @@ func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStd } } } else { - warning("Using --password via the CLI is insecure. Use --password-stdin.") + slog.Warn("using --password via the CLI is insecure. Use --password-stdin") } return username, password, nil diff --git a/pkg/helm/pkg/cmd/registry_login_test.go b/pkg/helm/pkg/cmd/registry_login_test.go new file mode 100644 index 00000000..6e4f2116 --- /dev/null +++ b/pkg/helm/pkg/cmd/registry_login_test.go @@ -0,0 +1,25 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestRegistryLoginFileCompletion(t *testing.T) { + checkFileCompletion(t, "registry login", false) +} diff --git a/pkg/helm/cmd/helm/registry_logout.go b/pkg/helm/pkg/cmd/registry_logout.go similarity index 87% rename from pkg/helm/cmd/helm/registry_logout.go rename to pkg/helm/pkg/cmd/registry_logout.go index 3c3d5738..29681f69 100644 --- a/pkg/helm/cmd/helm/registry_logout.go +++ b/pkg/helm/pkg/cmd/registry_logout.go @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "io" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" ) const registryLogoutDesc = ` @@ -35,8 +35,8 @@ func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Comma Short: "logout from a registry", Long: registryLogoutDesc, Args: require.MinimumNArgs(1), - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: cobra.NoFileCompletions, + RunE: func(_ *cobra.Command, args []string) error { hostname := args[0] return action.NewRegistryLogout(cfg).Run(out, hostname) }, diff --git a/pkg/helm/pkg/cmd/registry_logout_test.go b/pkg/helm/pkg/cmd/registry_logout_test.go new file mode 100644 index 00000000..31a21b27 --- /dev/null +++ b/pkg/helm/pkg/cmd/registry_logout_test.go @@ -0,0 +1,25 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestRegistryLogoutFileCompletion(t *testing.T) { + checkFileCompletion(t, "registry logout", false) +} diff --git a/pkg/helm/pkg/cmd/repo.go b/pkg/helm/pkg/cmd/repo.go new file mode 100644 index 00000000..1ad22604 --- /dev/null +++ b/pkg/helm/pkg/cmd/repo.go @@ -0,0 +1,54 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "errors" + "io" + "io/fs" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" +) + +var repoHelm = ` +This command consists of multiple subcommands to interact with chart repositories. + +It can be used to add, remove, list, and index chart repositories. +` + +func newRepoCmd(out io.Writer) *cobra.Command { + cmd := &cobra.Command{ + Use: "repo add|remove|list|index|update [ARGS]", + Short: "add, list, remove, update, and index chart repositories", + Long: repoHelm, + Args: require.NoArgs, + } + + cmd.AddCommand(newRepoAddCmd(out)) + cmd.AddCommand(newRepoListCmd(out)) + cmd.AddCommand(newRepoRemoveCmd(out)) + cmd.AddCommand(newRepoIndexCmd(out)) + cmd.AddCommand(newRepoUpdateCmd(out)) + + return cmd +} + +func isNotExist(err error) bool { + return errors.Is(err, fs.ErrNotExist) +} diff --git a/pkg/helm/cmd/helm/repo_add.go b/pkg/helm/pkg/cmd/repo_add.go similarity index 81% rename from pkg/helm/cmd/helm/repo_add.go rename to pkg/helm/pkg/cmd/repo_add.go index ab2c0fc9..cea00253 100644 --- a/pkg/helm/cmd/helm/repo_add.go +++ b/pkg/helm/pkg/cmd/repo_add.go @@ -14,26 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "context" + "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" "time" "github.com/gofrs/flock" - "github.com/pkg/errors" "github.com/spf13/cobra" "golang.org/x/term" "sigs.k8s.io/yaml" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/repo" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) // Repositories that have been permanently deleted and no longer work @@ -51,28 +52,31 @@ type repoAddOptions struct { passCredentialsAll bool forceUpdate bool allowDeprecatedRepos bool + timeout time.Duration certFile string keyFile string caFile string - insecureSkipTLSverify bool + insecureSkipTLSVerify bool repoFile string repoCache string - - // Deprecated, but cannot be removed until Helm 4 - deprecatedNoUpdate bool } func newRepoAddCmd(out io.Writer) *cobra.Command { o := &repoAddOptions{} cmd := &cobra.Command{ - Use: "add [NAME] [URL]", - Short: "add a chart repository", - Args: require.ExactArgs(2), - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + Use: "add [NAME] [URL]", + Short: "add a chart repository", + Args: require.ExactArgs(2), + ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + if len(args) > 1 { + return noMoreArgsComp() + } + return nil, cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(_ *cobra.Command, args []string) error { o.name = args[0] o.url = args[1] o.repoFile = settings.RepositoryConfig @@ -87,13 +91,13 @@ func newRepoAddCmd(out io.Writer) *cobra.Command { f.StringVar(&o.password, "password", "", "chart repository password") f.BoolVarP(&o.passwordFromStdinOpt, "password-stdin", "", false, "read chart repository password from stdin") f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists") - f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.") f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") - f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository") + f.BoolVar(&o.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository") f.BoolVar(&o.allowDeprecatedRepos, "allow-deprecated-repos", false, "by default, this command will not allow adding official repos that have been permanently deleted. This disables that behavior") f.BoolVar(&o.passCredentialsAll, "pass-credentials", false, "pass credentials to all domains") + f.DurationVar(&o.timeout, "timeout", getter.DefaultHTTPTimeout*time.Second, "time to wait for the index file download to complete") return cmd } @@ -134,7 +138,7 @@ func (o *repoAddOptions) run(out io.Writer) error { } b, err := os.ReadFile(o.repoFile) - if err != nil && !os.IsNotExist(err) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return err } @@ -173,12 +177,12 @@ func (o *repoAddOptions) run(out io.Writer) error { CertFile: o.certFile, KeyFile: o.keyFile, CAFile: o.caFile, - InsecureSkipTLSverify: o.insecureSkipTLSverify, + InsecureSkipTLSVerify: o.insecureSkipTLSVerify, } // Check if the repo name is legal if strings.Contains(o.name, "/") { - return errors.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name) + return fmt.Errorf("repository name (%s) contains '/', please specify a different name without '/'", o.name) } // If the repo exists do one of two things: @@ -187,10 +191,9 @@ func (o *repoAddOptions) run(out io.Writer) error { if !o.forceUpdate && f.Has(o.name) { existing := f.Get(o.name) if c != *existing { - // The input coming in for the name is different from what is already // configured. Return an error. - return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name) + return fmt.Errorf("repository name (%s) already exists, please specify a different name", o.name) } // The add is idempotent so do nothing @@ -198,7 +201,7 @@ func (o *repoAddOptions) run(out io.Writer) error { return nil } - r, err := repo.NewChartRepository(&c, getter.All(settings)) + r, err := repo.NewChartRepository(&c, getter.All(settings, getter.WithTimeout(o.timeout))) if err != nil { return err } @@ -207,12 +210,12 @@ func (o *repoAddOptions) run(out io.Writer) error { r.CachePath = o.repoCache } if _, err := r.DownloadIndexFile(); err != nil { - return errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", o.url) + return fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached: %w", o.url, err) } f.Update(&c) - if err := f.WriteFile(o.repoFile, 0600); err != nil { + if err := f.WriteFile(o.repoFile, 0o600); err != nil { return err } fmt.Fprintf(out, "%q has been added to your repositories\n", o.name) diff --git a/pkg/helm/cmd/helm/repo_add_test.go b/pkg/helm/pkg/cmd/repo_add_test.go similarity index 79% rename from pkg/helm/cmd/helm/repo_add_test.go rename to pkg/helm/pkg/cmd/repo_add_test.go index 438bac71..9b65cc2f 100644 --- a/pkg/helm/cmd/helm/repo_add_test.go +++ b/pkg/helm/pkg/cmd/repo_add_test.go @@ -14,11 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "errors" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" @@ -29,27 +31,26 @@ import ( "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/helmpath/xdg" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" ) func TestRepoAddCmd(t *testing.T) { - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) defer srv.Stop() // A second test server is setup to verify URL changing - srv2, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + srv2 := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) defer srv2.Stop() tmpdir := filepath.Join(t.TempDir(), "path-component.yaml/data") - err = os.MkdirAll(tmpdir, 0777) - if err != nil { + if err := os.MkdirAll(tmpdir, 0o777); err != nil { t.Fatal(err) } repoFile := filepath.Join(tmpdir, "repositories.yaml") @@ -81,10 +82,10 @@ func TestRepoAddCmd(t *testing.T) { } func TestRepoAdd(t *testing.T) { - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) defer ts.Stop() rootDir := t.TempDir() @@ -93,13 +94,12 @@ func TestRepoAdd(t *testing.T) { const testRepoName = "test-name" o := &repoAddOptions{ - name: testRepoName, - url: ts.URL(), - forceUpdate: false, - deprecatedNoUpdate: true, - repoFile: repoFile, + name: testRepoName, + url: ts.URL(), + forceUpdate: false, + repoFile: repoFile, } - os.Setenv(xdg.CacheHomeEnvVar, rootDir) + t.Setenv(xdg.CacheHomeEnvVar, rootDir) if err := o.run(io.Discard); err != nil { t.Error(err) @@ -115,11 +115,11 @@ func TestRepoAdd(t *testing.T) { } idx := filepath.Join(helmpath.CachePath("repository"), helmpath.CacheIndexFile(testRepoName)) - if _, err := os.Stat(idx); os.IsNotExist(err) { + if _, err := os.Stat(idx); errors.Is(err, fs.ErrNotExist) { t.Errorf("Error cache index file was not created for repository %s", testRepoName) } idx = filepath.Join(helmpath.CachePath("repository"), helmpath.CacheChartsFile(testRepoName)) - if _, err := os.Stat(idx); os.IsNotExist(err) { + if _, err := os.Stat(idx); errors.Is(err, fs.ErrNotExist) { t.Errorf("Error cache charts file was not created for repository %s", testRepoName) } @@ -135,10 +135,10 @@ func TestRepoAdd(t *testing.T) { } func TestRepoAddCheckLegalName(t *testing.T) { - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) defer ts.Stop() defer resetEnv()() @@ -148,13 +148,12 @@ func TestRepoAddCheckLegalName(t *testing.T) { repoFile := filepath.Join(t.TempDir(), "repositories.yaml") o := &repoAddOptions{ - name: testRepoName, - url: ts.URL(), - forceUpdate: false, - deprecatedNoUpdate: true, - repoFile: repoFile, + name: testRepoName, + url: ts.URL(), + forceUpdate: false, + repoFile: repoFile, } - os.Setenv(xdg.CacheHomeEnvVar, rootDir) + t.Setenv(xdg.CacheHomeEnvVar, rootDir) wantErrorMsg := fmt.Sprintf("repository name (%s) contains '/', please specify a different name without '/'", testRepoName) @@ -192,23 +191,23 @@ func TestRepoAddConcurrentHiddenFile(t *testing.T) { } func repoAddConcurrent(t *testing.T, testName, repoFile string) { - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + t.Helper() + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) defer ts.Stop() var wg sync.WaitGroup wg.Add(3) - for i := 0; i < 3; i++ { + for i := range 3 { go func(name string) { defer wg.Done() o := &repoAddOptions{ - name: name, - url: ts.URL(), - deprecatedNoUpdate: true, - forceUpdate: false, - repoFile: repoFile, + name: name, + url: ts.URL(), + forceUpdate: false, + repoFile: repoFile, } if err := o.run(io.Discard); err != nil { t.Error(err) @@ -228,7 +227,7 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) { } var name string - for i := 0; i < 3; i++ { + for i := range 3 { name = fmt.Sprintf("%s-%d", testName, i) if !f.Has(name) { t.Errorf("%s was not successfully inserted into %s: %s", name, repoFile, f.Repositories[0]) @@ -243,7 +242,11 @@ func TestRepoAddFileCompletion(t *testing.T) { } func TestRepoAddWithPasswordFromStdin(t *testing.T) { - srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/testserver/*.*") + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + repotest.WithMiddleware(repotest.BasicAuthMiddleware(t)), + ) defer srv.Stop() defer resetEnv()() diff --git a/pkg/helm/pkg/cmd/repo_index.go b/pkg/helm/pkg/cmd/repo_index.go new file mode 100644 index 00000000..fcdce458 --- /dev/null +++ b/pkg/helm/pkg/cmd/repo_index.go @@ -0,0 +1,122 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +const repoIndexDesc = ` +Read the current directory, generate an index file based on the charts found +and write the result to 'index.yaml' in the current directory. + +This tool is used for creating an 'index.yaml' file for a chart repository. To +set an absolute URL to the charts, use '--url' flag. + +To merge the generated index with an existing index file, use the '--merge' +flag. In this case, the charts found in the current directory will be merged +into the index passed in with --merge, with local charts taking priority over +existing charts. +` + +type repoIndexOptions struct { + dir string + url string + merge string + json bool +} + +func newRepoIndexCmd(out io.Writer) *cobra.Command { + o := &repoIndexOptions{} + + cmd := &cobra.Command{ + Use: "index [DIR]", + Short: "generate an index file given a directory containing packaged charts", + Long: repoIndexDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + // Allow file completion when completing the argument for the directory + return nil, cobra.ShellCompDirectiveDefault + } + // No more completions, so disable file completion + return noMoreArgsComp() + }, + RunE: func(_ *cobra.Command, args []string) error { + o.dir = args[0] + return o.run(out) + }, + } + + f := cmd.Flags() + f.StringVar(&o.url, "url", "", "url of chart repository") + f.StringVar(&o.merge, "merge", "", "merge the generated index into the given index") + f.BoolVar(&o.json, "json", false, "output in JSON format") + + return cmd +} + +func (i *repoIndexOptions) run(_ io.Writer) error { + path, err := filepath.Abs(i.dir) + if err != nil { + return err + } + + return index(path, i.url, i.merge, i.json) +} + +func index(dir, url, mergeTo string, json bool) error { + out := filepath.Join(dir, "index.yaml") + + i, err := repo.IndexDirectory(dir, url) + if err != nil { + return err + } + if mergeTo != "" { + // if index.yaml is missing then create an empty one to merge into + var i2 *repo.IndexFile + if _, err := os.Stat(mergeTo); errors.Is(err, fs.ErrNotExist) { + i2 = repo.NewIndexFile() + writeIndexFile(i2, mergeTo, json) + } else { + i2, err = repo.LoadIndexFile(mergeTo) + if err != nil { + return fmt.Errorf("merge failed: %w", err) + } + } + i.Merge(i2) + } + i.SortEntries() + return writeIndexFile(i, out, json) +} + +func writeIndexFile(i *repo.IndexFile, out string, json bool) error { + if json { + return i.WriteJSONFile(out, 0o644) + } + return i.WriteFile(out, 0o644) +} diff --git a/pkg/helm/pkg/cmd/repo_index_test.go b/pkg/helm/pkg/cmd/repo_index_test.go new file mode 100644 index 00000000..0f4ee95d --- /dev/null +++ b/pkg/helm/pkg/cmd/repo_index_test.go @@ -0,0 +1,194 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "encoding/json" + "io" + "os" + "path/filepath" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +func TestRepoIndexCmd(t *testing.T) { + + dir := t.TempDir() + + comp := filepath.Join(dir, "compressedchart-0.1.0.tgz") + if err := linkOrCopy("testdata/testcharts/compressedchart-0.1.0.tgz", comp); err != nil { + t.Fatal(err) + } + comp2 := filepath.Join(dir, "compressedchart-0.2.0.tgz") + if err := linkOrCopy("testdata/testcharts/compressedchart-0.2.0.tgz", comp2); err != nil { + t.Fatal(err) + } + + buf := bytes.NewBuffer(nil) + c := newRepoIndexCmd(buf) + + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + destIndex := filepath.Join(dir, "index.yaml") + + index, err := repo.LoadIndexFile(destIndex) + if err != nil { + t.Fatal(err) + } + + if len(index.Entries) != 1 { + t.Errorf("expected 1 entry, got %d: %#v", len(index.Entries), index.Entries) + } + + vs := index.Entries["compressedchart"] + if len(vs) != 2 { + t.Errorf("expected 2 versions, got %d: %#v", len(vs), vs) + } + + expectedVersion := "0.2.0" + if vs[0].Version != expectedVersion { + t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) + } + + b, err := os.ReadFile(destIndex) + if err != nil { + t.Fatal(err) + } + if json.Valid(b) { + t.Error("did not expect index file to be valid json") + } + + // Test with `--json` + + c.ParseFlags([]string{"--json", "true"}) + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + if b, err = os.ReadFile(destIndex); err != nil { + t.Fatal(err) + } + if !json.Valid(b) { + t.Error("index file is not valid json") + } + + // Test with `--merge` + + // Remove first two charts. + if err := os.Remove(comp); err != nil { + t.Fatal(err) + } + if err := os.Remove(comp2); err != nil { + t.Fatal(err) + } + // Add a new chart and a new version of an existing chart + if err := linkOrCopy("testdata/testcharts/reqtest-0.1.0.tgz", filepath.Join(dir, "reqtest-0.1.0.tgz")); err != nil { + t.Fatal(err) + } + if err := linkOrCopy("testdata/testcharts/compressedchart-0.3.0.tgz", filepath.Join(dir, "compressedchart-0.3.0.tgz")); err != nil { + t.Fatal(err) + } + + c.ParseFlags([]string{"--merge", destIndex}) + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + index, err = repo.LoadIndexFile(destIndex) + if err != nil { + t.Fatal(err) + } + + if len(index.Entries) != 2 { + t.Errorf("expected 2 entries, got %d: %#v", len(index.Entries), index.Entries) + } + + vs = index.Entries["compressedchart"] + if len(vs) != 3 { + t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs) + } + + expectedVersion = "0.3.0" + if vs[0].Version != expectedVersion { + t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) + } + + // test that index.yaml gets generated on merge even when it doesn't exist + if err := os.Remove(destIndex); err != nil { + t.Fatal(err) + } + + c.ParseFlags([]string{"--merge", destIndex}) + if err := c.RunE(c, []string{dir}); err != nil { + t.Error(err) + } + + index, err = repo.LoadIndexFile(destIndex) + if err != nil { + t.Fatal(err) + } + + // verify it didn't create an empty index.yaml and the merged happened + if len(index.Entries) != 2 { + t.Errorf("expected 2 entries, got %d: %#v", len(index.Entries), index.Entries) + } + + vs = index.Entries["compressedchart"] + if len(vs) != 1 { + t.Errorf("expected 1 versions, got %d: %#v", len(vs), vs) + } + + expectedVersion = "0.3.0" + if vs[0].Version != expectedVersion { + t.Errorf("expected %q, got %q", expectedVersion, vs[0].Version) + } +} + +func linkOrCopy(source, target string) error { + if err := os.Link(source, target); err != nil { + return copyFile(source, target) + } + + return nil +} + +func copyFile(dst, src string) error { + i, err := os.Open(dst) + if err != nil { + return err + } + defer i.Close() + + o, err := os.Create(src) + if err != nil { + return err + } + defer o.Close() + + _, err = io.Copy(o, i) + + return err +} + +func TestRepoIndexFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo index", true) + checkFileCompletion(t, "repo index mydir", false) +} diff --git a/pkg/helm/cmd/helm/repo_list.go b/pkg/helm/pkg/cmd/repo_list.go similarity index 75% rename from pkg/helm/cmd/helm/repo_list.go rename to pkg/helm/pkg/cmd/repo_list.go index c28908d0..ebb465d8 100644 --- a/pkg/helm/cmd/helm/repo_list.go +++ b/pkg/helm/pkg/cmd/repo_list.go @@ -14,41 +14,50 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" "io" "github.com/gosuri/uitable" - "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/cli/output" - "github.com/werf/nelm/pkg/helm/pkg/repo" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) func newRepoListCmd(out io.Writer) *cobra.Command { var outfmt output.Format + var noHeaders bool cmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list chart repositories", Args: require.NoArgs, - ValidArgsFunction: noCompletions, - RunE: func(cmd *cobra.Command, args []string) error { + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(cmd *cobra.Command, _ []string) error { + // The error is silently ignored. If no repository file exists, it cannot be loaded, + // or the file isn't the right format to be parsed the error is ignored. The + // repositories will be 0. f, _ := repo.LoadFile(settings.RepositoryConfig) - if len(f.Repositories) == 0 && !(outfmt == output.JSON || outfmt == output.YAML) { - return errors.New("no repositories to show") + if len(f.Repositories) == 0 && outfmt != output.JSON && outfmt != output.YAML { + fmt.Fprintln(cmd.ErrOrStderr(), "no repositories to show") + return nil } - return outfmt.Write(out, &repoListWriter{f.Repositories}) + w := &repoListWriter{ + repos: f.Repositories, + noHeaders: noHeaders, + } + + return outfmt.Write(out, w) }, } + cmd.Flags().BoolVar(&noHeaders, "no-headers", false, "suppress headers in the output") bindOutputFlag(cmd, &outfmt) - return cmd } @@ -58,12 +67,15 @@ type repositoryElement struct { } type repoListWriter struct { - repos []*repo.Entry + repos []*repo.Entry + noHeaders bool } func (r *repoListWriter) WriteTable(out io.Writer) error { table := uitable.New() - table.AddRow("NAME", "URL") + if !r.noHeaders { + table.AddRow("NAME", "URL") + } for _, re := range r.repos { table.AddRow(re.Name, re.URL) } @@ -91,11 +103,11 @@ func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) err return output.EncodeJSON(out, repolist) case output.YAML: return output.EncodeYAML(out, repolist) + default: + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil } - - // Because this is a non-exported function and only called internally by - // WriteJSON and WriteYAML, we shouldn't get invalid types - return nil } // Returns all repos from repos, except those with names matching ignoredRepoNames diff --git a/pkg/helm/pkg/cmd/repo_list_test.go b/pkg/helm/pkg/cmd/repo_list_test.go new file mode 100644 index 00000000..94cdf396 --- /dev/null +++ b/pkg/helm/pkg/cmd/repo_list_test.go @@ -0,0 +1,60 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "path/filepath" + "testing" +) + +func TestRepoListOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "repo list") +} + +func TestRepoListFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo list", false) +} + +func TestRepoList(t *testing.T) { + rootDir := t.TempDir() + repoFile := filepath.Join(rootDir, "repositories.yaml") + repoFile2 := "testdata/repositories.yaml" + + tests := []cmdTestCase{ + { + name: "list with no repos", + cmd: fmt.Sprintf("repo list --repository-config %s --repository-cache %s", repoFile, rootDir), + golden: "output/repo-list-empty.txt", + wantError: false, + }, + { + name: "list with repos", + cmd: fmt.Sprintf("repo list --repository-config %s --repository-cache %s", repoFile2, rootDir), + golden: "output/repo-list.txt", + wantError: false, + }, + { + name: "list without headers", + cmd: fmt.Sprintf("repo list --repository-config %s --repository-cache %s --no-headers", repoFile2, rootDir), + golden: "output/repo-list-no-headers.txt", + wantError: false, + }, + } + + runTestCmd(t, tests) +} diff --git a/pkg/helm/cmd/helm/repo_remove.go b/pkg/helm/pkg/cmd/repo_remove.go similarity index 81% rename from pkg/helm/cmd/helm/repo_remove.go rename to pkg/helm/pkg/cmd/repo_remove.go index 9bc50309..df855a9f 100644 --- a/pkg/helm/cmd/helm/repo_remove.go +++ b/pkg/helm/pkg/cmd/repo_remove.go @@ -14,20 +14,21 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "errors" "fmt" "io" + "io/fs" "os" "path/filepath" - "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/repo" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) type repoRemoveOptions struct { @@ -44,10 +45,10 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command { Aliases: []string{"rm"}, Short: "remove one or more chart repositories", Args: require.MinimumNArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp }, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { o.repoFile = settings.RepositoryConfig o.repoCache = settings.RepositoryCache o.names = args @@ -65,7 +66,7 @@ func (o *repoRemoveOptions) run(out io.Writer) error { for _, name := range o.names { if !r.Remove(name) { - return errors.Errorf("no repo named %q found", name) + return fmt.Errorf("no repo named %q found", name) } if err := r.WriteFile(o.repoFile, 0600); err != nil { return err @@ -87,10 +88,10 @@ func removeRepoCache(root, name string) error { } idx = filepath.Join(root, helmpath.CacheIndexFile(name)) - if _, err := os.Stat(idx); os.IsNotExist(err) { + if _, err := os.Stat(idx); errors.Is(err, fs.ErrNotExist) { return nil } else if err != nil { - return errors.Wrapf(err, "can't remove index file %s", idx) + return fmt.Errorf("can't remove index file %s: %w", idx, err) } return os.Remove(idx) } diff --git a/pkg/helm/cmd/helm/repo_remove_test.go b/pkg/helm/pkg/cmd/repo_remove_test.go similarity index 94% rename from pkg/helm/cmd/helm/repo_remove_test.go rename to pkg/helm/pkg/cmd/repo_remove_test.go index 80fa6ced..c86bef9d 100644 --- a/pkg/helm/cmd/helm/repo_remove_test.go +++ b/pkg/helm/pkg/cmd/repo_remove_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "bytes" @@ -25,15 +25,15 @@ import ( "testing" "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" ) func TestRepoRemove(t *testing.T) { - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) defer ts.Stop() rootDir := t.TempDir() @@ -153,6 +153,7 @@ func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, c } func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, repoName string) { + t.Helper() if _, err := os.Stat(cacheIndexFile); err == nil { t.Errorf("Error cache index file was not removed for repository %s", repoName) } @@ -162,10 +163,11 @@ func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, } func TestRepoRemoveCompletion(t *testing.T) { - ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*") - if err != nil { - t.Fatal(err) - } + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) + defer ts.Stop() rootDir := t.TempDir() diff --git a/pkg/helm/pkg/cmd/repo_test.go b/pkg/helm/pkg/cmd/repo_test.go new file mode 100644 index 00000000..6b89a66c --- /dev/null +++ b/pkg/helm/pkg/cmd/repo_test.go @@ -0,0 +1,25 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestRepoFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo", false) +} diff --git a/pkg/helm/pkg/cmd/repo_update.go b/pkg/helm/pkg/cmd/repo_update.go new file mode 100644 index 00000000..f5547fc2 --- /dev/null +++ b/pkg/helm/pkg/cmd/repo_update.go @@ -0,0 +1,176 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "errors" + "fmt" + "io" + "slices" + "sync" + "time" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + "github.com/werf/nelm/pkg/helm/pkg/getter" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +const updateDesc = ` +Update gets the latest information about charts from the respective chart repositories. +Information is cached locally, where it is used by commands like 'helm search'. + +You can optionally specify a list of repositories you want to update. + $ helm repo update ... +To update all the repositories, use 'helm repo update'. +` + +var errNoRepositories = errors.New("no repositories found. You must add one before updating") + +type repoUpdateOptions struct { + update func([]*repo.ChartRepository, io.Writer) error + repoFile string + repoCache string + names []string + timeout time.Duration +} + +func newRepoUpdateCmd(out io.Writer) *cobra.Command { + o := &repoUpdateOptions{update: updateCharts} + + cmd := &cobra.Command{ + Use: "update [REPO1 [REPO2 ...]]", + Aliases: []string{"up"}, + Short: "update information of available charts locally from chart repositories", + Long: updateDesc, + Args: require.MinimumNArgs(0), + ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return compListRepos(toComplete, args), cobra.ShellCompDirectiveNoFileComp + }, + RunE: func(_ *cobra.Command, args []string) error { + o.repoFile = settings.RepositoryConfig + o.repoCache = settings.RepositoryCache + o.names = args + return o.run(out) + }, + } + + f := cmd.Flags() + f.DurationVar(&o.timeout, "timeout", getter.DefaultHTTPTimeout*time.Second, "time to wait for the index file download to complete") + + return cmd +} + +func (o *repoUpdateOptions) run(out io.Writer) error { + f, err := repo.LoadFile(o.repoFile) + switch { + case isNotExist(err): + return errNoRepositories + case err != nil: + return fmt.Errorf("failed loading file: %s: %w", o.repoFile, err) + case len(f.Repositories) == 0: + return errNoRepositories + } + + var repos []*repo.ChartRepository + updateAllRepos := len(o.names) == 0 + + if !updateAllRepos { + // Fail early if the user specified an invalid repo to update + if err := checkRequestedRepos(o.names, f.Repositories); err != nil { + return err + } + } + + for _, cfg := range f.Repositories { + if updateAllRepos || isRepoRequested(cfg.Name, o.names) { + r, err := repo.NewChartRepository(cfg, getter.All(settings, getter.WithTimeout(o.timeout))) + if err != nil { + return err + } + if o.repoCache != "" { + r.CachePath = o.repoCache + } + repos = append(repos, r) + } + } + + return o.update(repos, out) +} + +func updateCharts(repos []*repo.ChartRepository, out io.Writer) error { + fmt.Fprintln(out, "Hang tight while we grab the latest from your chart repositories...") + var wg sync.WaitGroup + failRepoURLChan := make(chan string, len(repos)) + + writeMutex := sync.Mutex{} + for _, re := range repos { + wg.Add(1) + go func(re *repo.ChartRepository) { + defer wg.Done() + if _, err := re.DownloadIndexFile(); err != nil { + writeMutex.Lock() + defer writeMutex.Unlock() + fmt.Fprintf(out, "...Unable to get an update from the %q chart repository (%s):\n\t%s\n", re.Config.Name, re.Config.URL, err) + failRepoURLChan <- re.Config.URL + } else { + writeMutex.Lock() + defer writeMutex.Unlock() + fmt.Fprintf(out, "...Successfully got an update from the %q chart repository\n", re.Config.Name) + } + }(re) + } + + go func() { + wg.Wait() + close(failRepoURLChan) + }() + + var repoFailList []string + for url := range failRepoURLChan { + repoFailList = append(repoFailList, url) + } + + if len(repoFailList) > 0 { + return fmt.Errorf("failed to update the following repositories: %s", + repoFailList) + } + + fmt.Fprintln(out, "Update Complete. ⎈Happy Helming!⎈") + return nil +} + +func checkRequestedRepos(requestedRepos []string, validRepos []*repo.Entry) error { + for _, requestedRepo := range requestedRepos { + found := false + for _, repo := range validRepos { + if requestedRepo == repo.Name { + found = true + break + } + } + if !found { + return fmt.Errorf("no repositories found matching '%s'. Nothing will be updated", requestedRepo) + } + } + return nil +} + +func isRepoRequested(repoName string, requestedRepos []string) bool { + return slices.Contains(requestedRepos, repoName) +} diff --git a/pkg/helm/pkg/cmd/repo_update_test.go b/pkg/helm/pkg/cmd/repo_update_test.go new file mode 100644 index 00000000..411e46fa --- /dev/null +++ b/pkg/helm/pkg/cmd/repo_update_test.go @@ -0,0 +1,212 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/werf/nelm/pkg/helm/intern/test/ensure" + "github.com/werf/nelm/pkg/helm/pkg/getter" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" +) + +func TestUpdateCmd(t *testing.T) { + var out bytes.Buffer + // Instead of using the HTTP updater, we provide our own for this test. + // The TestUpdateCharts test verifies the HTTP behavior independently. + updater := func(repos []*repo.ChartRepository, out io.Writer) error { + for _, re := range repos { + fmt.Fprintln(out, re.Config.Name) + } + return nil + } + o := &repoUpdateOptions{ + update: updater, + repoFile: "testdata/repositories.yaml", + } + if err := o.run(&out); err != nil { + t.Fatal(err) + } + + if got := out.String(); !strings.Contains(got, "charts") || + !strings.Contains(got, "firstexample") || + !strings.Contains(got, "secondexample") { + t.Errorf("Expected 'charts', 'firstexample' and 'secondexample' but got %q", got) + } +} + +func TestUpdateCmdMultiple(t *testing.T) { + var out bytes.Buffer + // Instead of using the HTTP updater, we provide our own for this test. + // The TestUpdateCharts test verifies the HTTP behavior independently. + updater := func(repos []*repo.ChartRepository, out io.Writer) error { + for _, re := range repos { + fmt.Fprintln(out, re.Config.Name) + } + return nil + } + o := &repoUpdateOptions{ + update: updater, + repoFile: "testdata/repositories.yaml", + names: []string{"firstexample", "charts"}, + } + if err := o.run(&out); err != nil { + t.Fatal(err) + } + + if got := out.String(); !strings.Contains(got, "charts") || + !strings.Contains(got, "firstexample") || + strings.Contains(got, "secondexample") { + t.Errorf("Expected 'charts' and 'firstexample' but not 'secondexample' but got %q", got) + } +} + +func TestUpdateCmdInvalid(t *testing.T) { + var out bytes.Buffer + // Instead of using the HTTP updater, we provide our own for this test. + // The TestUpdateCharts test verifies the HTTP behavior independently. + updater := func(repos []*repo.ChartRepository, out io.Writer) error { + for _, re := range repos { + fmt.Fprintln(out, re.Config.Name) + } + return nil + } + o := &repoUpdateOptions{ + update: updater, + repoFile: "testdata/repositories.yaml", + names: []string{"firstexample", "invalid"}, + } + if err := o.run(&out); err == nil { + t.Fatal("expected error but did not get one") + } +} + +func TestUpdateCustomCacheCmd(t *testing.T) { + rootDir := t.TempDir() + cachePath := filepath.Join(rootDir, "updcustomcache") + os.Mkdir(cachePath, os.ModePerm) + + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) + + defer ts.Stop() + + o := &repoUpdateOptions{ + update: updateCharts, + repoFile: filepath.Join(ts.Root(), "repositories.yaml"), + repoCache: cachePath, + } + b := io.Discard + if err := o.run(b); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(cachePath, "test-index.yaml")); err != nil { + t.Fatalf("error finding created index file in custom cache: %v", err) + } +} + +func TestUpdateCharts(t *testing.T) { + defer resetEnv()() + ensure.HelmHome(t) + + ts := repotest.NewTempServer(t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) + defer ts.Stop() + + r, err := repo.NewChartRepository(&repo.Entry{ + Name: "charts", + URL: ts.URL(), + }, getter.All(settings)) + if err != nil { + t.Error(err) + } + + b := bytes.NewBuffer(nil) + updateCharts([]*repo.ChartRepository{r}, b) + + got := b.String() + if strings.Contains(got, "Unable to get an update") { + t.Errorf("Failed to get a repo: %q", got) + } + if !strings.Contains(got, "Update Complete.") { + t.Error("Update was not successful") + } +} + +func TestRepoUpdateFileCompletion(t *testing.T) { + checkFileCompletion(t, "repo update", false) + checkFileCompletion(t, "repo update repo1", false) +} + +func TestUpdateChartsFailWithError(t *testing.T) { + defer resetEnv()() + ensure.HelmHome(t) + + ts := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testserver/*.*"), + ) + defer ts.Stop() + + var invalidURL = ts.URL() + "55" + r1, err := repo.NewChartRepository(&repo.Entry{ + Name: "charts", + URL: invalidURL, + }, getter.All(settings)) + if err != nil { + t.Error(err) + } + r2, err := repo.NewChartRepository(&repo.Entry{ + Name: "charts", + URL: invalidURL, + }, getter.All(settings)) + if err != nil { + t.Error(err) + } + + b := bytes.NewBuffer(nil) + err = updateCharts([]*repo.ChartRepository{r1, r2}, b) + if err == nil { + t.Error("Repo update should return error because update of repository fails and 'fail-on-repo-update-fail' flag set") + return + } + var expectedErr = "failed to update the following repositories" + var receivedErr = err.Error() + if !strings.Contains(receivedErr, expectedErr) { + t.Errorf("Expected error (%s) but got (%s) instead", expectedErr, receivedErr) + } + if !strings.Contains(receivedErr, invalidURL) { + t.Errorf("Expected invalid URL (%s) in error message but got (%s) instead", invalidURL, receivedErr) + } + + got := b.String() + if !strings.Contains(got, "Unable to get an update") { + t.Errorf("Repo should have failed update but instead got: %q", got) + } + if strings.Contains(got, "Update Complete.") { + t.Error("Update was not successful and should return error message because 'fail-on-repo-update-fail' flag set") + } +} diff --git a/pkg/helm/cmd/helm/require/args.go b/pkg/helm/pkg/cmd/require/args.go similarity index 94% rename from pkg/helm/cmd/helm/require/args.go rename to pkg/helm/pkg/cmd/require/args.go index cfa8a016..f5e0888f 100644 --- a/pkg/helm/cmd/helm/require/args.go +++ b/pkg/helm/pkg/cmd/require/args.go @@ -16,14 +16,15 @@ limitations under the License. package require import ( - "github.com/pkg/errors" + "fmt" + "github.com/spf13/cobra" ) // NoArgs returns an error if any args are included. func NoArgs(cmd *cobra.Command, args []string) error { if len(args) > 0 { - return errors.Errorf( + return fmt.Errorf( "%q accepts no arguments\n\nUsage: %s", cmd.CommandPath(), cmd.UseLine(), @@ -36,7 +37,7 @@ func NoArgs(cmd *cobra.Command, args []string) error { func ExactArgs(n int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != n { - return errors.Errorf( + return fmt.Errorf( "%q requires %d %s\n\nUsage: %s", cmd.CommandPath(), n, @@ -52,7 +53,7 @@ func ExactArgs(n int) cobra.PositionalArgs { func MaximumNArgs(n int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) > n { - return errors.Errorf( + return fmt.Errorf( "%q accepts at most %d %s\n\nUsage: %s", cmd.CommandPath(), n, @@ -68,7 +69,7 @@ func MaximumNArgs(n int) cobra.PositionalArgs { func MinimumNArgs(n int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) < n { - return errors.Errorf( + return fmt.Errorf( "%q requires at least %d %s\n\nUsage: %s", cmd.CommandPath(), n, diff --git a/pkg/helm/cmd/helm/require/args_test.go b/pkg/helm/pkg/cmd/require/args_test.go similarity index 93% rename from pkg/helm/cmd/helm/require/args_test.go rename to pkg/helm/pkg/cmd/require/args_test.go index 5a84a42d..3dbd4df7 100644 --- a/pkg/helm/cmd/helm/require/args_test.go +++ b/pkg/helm/pkg/cmd/require/args_test.go @@ -63,6 +63,7 @@ type testCase struct { } func runTestCases(t *testing.T, testCases []testCase) { + t.Helper() for i, tc := range testCases { t.Run(fmt.Sprint(i), func(t *testing.T) { cmd := &cobra.Command{ @@ -70,8 +71,13 @@ func runTestCases(t *testing.T, testCases []testCase) { Run: func(*cobra.Command, []string) {}, Args: tc.validateFunc, } - cmd.SetArgs(tc.args) - cmd.SetOutput(io.Discard) + if tc.args != nil { + cmd.SetArgs(tc.args) + } else { + cmd.SetArgs([]string{}) + } + cmd.SetOut(io.Discard) + cmd.SetErr(io.Discard) err := cmd.Execute() if tc.wantError == "" { diff --git a/pkg/helm/pkg/cmd/root.go b/pkg/helm/pkg/cmd/root.go new file mode 100644 index 00000000..52e58b83 --- /dev/null +++ b/pkg/helm/pkg/cmd/root.go @@ -0,0 +1,481 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd // import "github.com/werf/nelm/pkg/helm/pkg/cmd" + +import ( + "context" + "fmt" + "io" + "log" + "log/slog" + "net/http" + "os" + "strings" + + "github.com/fatih/color" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" + + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/intern/tlsutil" + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cli" + kubefake "github.com/werf/nelm/pkg/helm/pkg/kube/fake" + "github.com/werf/nelm/pkg/helm/pkg/registry" + ri "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/storage/driver" +) + +var globalUsage = `The Kubernetes package manager + +Common actions for Helm: + +- helm search: search for charts +- helm pull: download a chart to your local directory to view +- helm install: upload the chart to Kubernetes +- helm list: list releases of charts + +Environment variables: + +| Name | Description | +|------------------------------------|------------------------------------------------------------------------------------------------------------| +| $HELM_CACHE_HOME | set an alternative location for storing cached files. | +| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | +| $HELM_DATA_HOME | set an alternative location for storing Helm data. | +| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode | +| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. | +| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | +| $HELM_MAX_HISTORY | set the maximum number of helm release history. | +| $HELM_NAMESPACE | set the namespace used for the helm operations. | +| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. | +| $HELM_PLUGINS | set the path to the plugins directory | +| $HELM_REGISTRY_CONFIG | set the path to the registry config file. | +| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory | +| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. | +| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") | +| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication | +| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. | +| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. | +| $HELM_KUBEASUSER | set the Username to impersonate for the operation. | +| $HELM_KUBECONTEXT | set the name of the kubeconfig context. | +| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. | +| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) | +| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate | +| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable) | +| $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values | +| $HELM_COLOR | set color output mode. Allowed values: never, always, auto (default: never) | +| $NO_COLOR | set to any non-empty value to disable all colored output (overrides $HELM_COLOR) | + +Helm stores cache, configuration, and data based on the following configuration order: + +- If a HELM_*_HOME environment variable is set, it will be used +- Otherwise, on systems supporting the XDG base directory specification, the XDG variables will be used +- When no other location is set a default location will be used based on the operating system + +By default, the default directories depend on the Operating System. The defaults are listed below: + +| Operating System | Cache Path | Configuration Path | Data Path | +|------------------|---------------------------|--------------------------------|-------------------------| +| Linux | $HOME/.cache/helm | $HOME/.config/helm | $HOME/.local/share/helm | +| macOS | $HOME/Library/Caches/helm | $HOME/Library/Preferences/helm | $HOME/Library/helm | +| Windows | %TEMP%\helm | %APPDATA%\helm | %APPDATA%\helm | +` + +var settings = cli.New() + +var Settings = settings + +func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { + actionConfig := action.NewConfiguration() + cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup) + if err != nil { + return nil, err + } + cobra.OnInitialize(func() { + helmDriver := os.Getenv("HELM_DRIVER") + if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver); err != nil { + log.Fatal(err) + } + if helmDriver == "memory" { + loadReleasesInMemory(actionConfig) + } + actionConfig.SetHookOutputFunc(hookOutputWriter) + }) + return cmd, nil +} + +// SetupLogging sets up Helm logging used by the Helm client. +// This function is passed to the NewRootCmd function to enable logging. Any other +// application that uses the NewRootCmd function to setup all the Helm commands may +// use this function to setup logging or their own. Using a custom logging setup function +// enables applications using Helm commands to integrate with their existing logging +// system. +// The debug argument is the value if Helm is set for debugging (i.e. --debug flag) +func SetupLogging(debug bool) { + logger := logging.NewLogger(func() bool { return debug }) + slog.SetDefault(logger) +} + +// configureColorOutput configures the color output based on the ColorMode setting +func configureColorOutput(settings *cli.EnvSettings) { + switch settings.ColorMode { + case "never": + color.NoColor = true + case "always": + color.NoColor = false + case "auto": + // Let fatih/color handle automatic detection + // It will check if output is a terminal and NO_COLOR env var + // We don't need to do anything here + } +} + +func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { + cmd := &cobra.Command{ + Use: "helm", + Short: "The Helm package manager for Kubernetes.", + Long: globalUsage, + SilenceUsage: true, + PersistentPreRun: func(_ *cobra.Command, _ []string) {}, + PersistentPostRun: func(_ *cobra.Command, _ []string) {}, + } + + flags := cmd.PersistentFlags() + + settings.AddFlags(flags) + addKlogFlags(flags) + + // We can safely ignore any errors that flags.Parse encounters since + // those errors will be caught later during the call to cmd.Execution. + // This call is required to gather configuration information prior to + // execution. + flags.ParseErrorsAllowlist.UnknownFlags = true + flags.Parse(args) + + logSetup(settings.Debug) + + // newRootCmdWithConfig is only called from NewRootCmd. NewRootCmd sets up + // NewConfiguration without a custom logger. So, the slog default is used. logSetup + // can change the default logger to the one in the logger package. This happens for + // the Helm client. This means the actionConfig logger is different from the slog + // default logger. If they are different we sync the actionConfig logger to the slog + // current default one. + if actionConfig.Logger() != slog.Default() { + actionConfig.SetLogger(slog.Default().Handler()) + } + + // Validate color mode setting + switch settings.ColorMode { + case "never", "auto", "always": + // Valid color mode + default: + return nil, fmt.Errorf("invalid color mode %q: must be one of: never, auto, always", settings.ColorMode) + } + + // Configure color output based on ColorMode setting + configureColorOutput(settings) + + // Setup shell completion for the color flag + _ = cmd.RegisterFlagCompletionFunc("color", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"never", "auto", "always"}, cobra.ShellCompDirectiveNoFileComp + }) + + // Setup shell completion for the colour flag + _ = cmd.RegisterFlagCompletionFunc("colour", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + return []string{"never", "auto", "always"}, cobra.ShellCompDirectiveNoFileComp + }) + + // Setup shell completion for the namespace flag + err := cmd.RegisterFlagCompletionFunc("namespace", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + if client, err := actionConfig.KubernetesClientSet(); err == nil { + // Choose a long enough timeout that the user notices something is not working + // but short enough that the user is not made to wait very long + to := int64(3) + cobra.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to), settings.Debug) + + nsNames := []string{} + if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { + for _, ns := range namespaces.Items { + nsNames = append(nsNames, ns.Name) + } + return nsNames, cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault + }) + + if err != nil { + log.Fatal(err) + } + + // Setup shell completion for the kube-context flag + err = cmd.RegisterFlagCompletionFunc("kube-context", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { + cobra.CompDebugln("About to get the different kube-contexts", settings.Debug) + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + if len(settings.KubeConfig) > 0 { + loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig} + } + if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + loadingRules, + &clientcmd.ConfigOverrides{}).RawConfig(); err == nil { + comps := []string{} + for name, context := range config.Contexts { + comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster)) + } + return comps, cobra.ShellCompDirectiveNoFileComp + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + + if err != nil { + log.Fatal(err) + } + + registryClient, err := newDefaultRegistryClient(false, "", "") + if err != nil { + return nil, err + } + actionConfig.RegistryClient = registryClient + + cmd.AddCommand( + newCreateCmd(out), + newDependencyCmd(actionConfig, out), + newPullCmd(actionConfig, out), + newShowCmd(actionConfig, out), + newPackageCmd(out), + newRepoCmd(out), + newSearchCmd(out), + newVerifyCmd(out), + + newHistoryCmd(actionConfig, out), + newStatusCmd(actionConfig, out), + + newCompletionCmd(out), + newEnvCmd(out), + newVersionCmd(out), + ) + + cmd.AddCommand( + newRegistryCmd(actionConfig, out), + newPushCmd(actionConfig, out), + ) + + // Check for expired repositories + checkForExpiredRepos(settings.RepositoryConfig) + + return cmd, nil +} + +// This function loads releases into the memory storage if the +// environment variable is properly set. +func loadReleasesInMemory(actionConfig *action.Configuration) { + filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":") + if len(filePaths) == 0 { + return + } + + store := actionConfig.Releases + mem, ok := store.Driver.(*driver.Memory) + if !ok { + // For an unexpected reason we are not dealing with the memory storage driver. + return + } + + actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard} + + for _, path := range filePaths { + b, err := os.ReadFile(path) + if err != nil { + log.Fatal("Unable to read memory driver data", err) + } + + releases := []*release.Release{} + if err := yaml.Unmarshal(b, &releases); err != nil { + log.Fatal("Unable to unmarshal memory driver data: ", err) + } + + for _, rel := range releases { + if err := store.Create(rel); err != nil { + log.Fatal(err) + } + } + } + // Must reset namespace to the proper one + mem.SetNamespace(settings.Namespace()) +} + +// hookOutputWriter provides the writer for writing hook logs. +func hookOutputWriter(_, _, _ string) io.Writer { + return log.Writer() +} + +func checkForExpiredRepos(repofile string) { + + expiredRepos := []struct { + name string + old string + new string + }{ + { + name: "stable", + old: "kubernetes-charts.storage.googleapis.com", + new: "https://charts.helm.sh/stable", + }, + { + name: "incubator", + old: "kubernetes-charts-incubator.storage.googleapis.com", + new: "https://charts.helm.sh/incubator", + }, + } + + // parse repo file. + // Ignore the error because it is okay for a repo file to be unparsable at this + // stage. Later checks will trap the error and respond accordingly. + repoFile, err := repo.LoadFile(repofile) + if err != nil { + return + } + + for _, exp := range expiredRepos { + r := repoFile.Get(exp.name) + if r == nil { + return + } + + if url := r.URL; strings.Contains(url, exp.old) { + fmt.Fprintf( + os.Stderr, + "WARNING: %q is deprecated for %q and will be deleted Nov. 13, 2020.\nWARNING: You should switch to %q via:\nWARNING: helm repo add %q %q --force-update\n", + exp.old, + exp.name, + exp.new, + exp.name, + exp.new, + ) + } + } + +} + +func newRegistryClient( + certFile, keyFile, caFile string, insecureSkipTLSVerify, plainHTTP bool, username, password string, +) (*registry.Client, error) { + if certFile != "" && keyFile != "" || caFile != "" || insecureSkipTLSVerify { + registryClient, err := newRegistryClientWithTLS(certFile, keyFile, caFile, insecureSkipTLSVerify, username, password) + if err != nil { + return nil, err + } + return registryClient, nil + } + registryClient, err := newDefaultRegistryClient(plainHTTP, username, password) + if err != nil { + return nil, err + } + return registryClient, nil +} + +func newDefaultRegistryClient(plainHTTP bool, username, password string) (*registry.Client, error) { + opts := []registry.ClientOption{ + registry.ClientOptDebug(settings.Debug), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(os.Stderr), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptBasicAuth(username, password), + } + if plainHTTP { + opts = append(opts, registry.ClientOptPlainHTTP()) + } + + // Create a new registry client + registryClient, err := registry.NewClient(opts...) + if err != nil { + return nil, err + } + return registryClient, nil +} + +func newRegistryClientWithTLS( + certFile, keyFile, caFile string, insecureSkipTLSVerify bool, username, password string, +) (*registry.Client, error) { + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify), + tlsutil.WithCertKeyPairFiles(certFile, keyFile), + tlsutil.WithCAFile(caFile), + ) + + if err != nil { + return nil, fmt.Errorf("can't create TLS config for client: %w", err) + } + + // Create a new registry client + registryClient, err := registry.NewClient( + registry.ClientOptDebug(settings.Debug), + registry.ClientOptEnableCache(true), + registry.ClientOptWriter(os.Stderr), + registry.ClientOptCredentialsFile(settings.RegistryConfig), + registry.ClientOptHTTPClient(&http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConf, + Proxy: http.ProxyFromEnvironment, + }, + }), + registry.ClientOptBasicAuth(username, password), + ) + if err != nil { + return nil, err + } + return registryClient, nil +} + +type CommandError struct { + error + ExitCode int +} + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel ri.Releaser) (*release.Release, error) { + switch r := rel.(type) { + case release.Release: + return &r, nil + case *release.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} + +func releaseListToV1List(ls []ri.Releaser) ([]*release.Release, error) { + rls := make([]*release.Release, 0, len(ls)) + for _, val := range ls { + rel, err := releaserToV1Release(val) + if err != nil { + return nil, err + } + rls = append(rls, rel) + } + + return rls, nil +} diff --git a/pkg/helm/cmd/helm/root_test.go b/pkg/helm/pkg/cmd/root_test.go similarity index 84% rename from pkg/helm/cmd/helm/root_test.go rename to pkg/helm/pkg/cmd/root_test.go index d4e71124..19595614 100644 --- a/pkg/helm/cmd/helm/root_test.go +++ b/pkg/helm/pkg/cmd/root_test.go @@ -14,14 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( + "bytes" + "log/slog" "os" "path/filepath" "testing" "github.com/werf/nelm/pkg/helm/intern/test/ensure" + "github.com/werf/nelm/pkg/helm/pkg/action" "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/helmpath/xdg" ) @@ -55,24 +58,6 @@ func TestRootCmd(t *testing.T) { envvars: map[string]string{xdg.DataHomeEnvVar: "/bar"}, dataPath: "/bar/helm", }, - { - name: "with $HELM_CACHE_HOME set", - args: "env", - envvars: map[string]string{helmpath.CacheHomeEnvVar: "/foo/helm"}, - cachePath: "/foo/helm", - }, - { - name: "with $HELM_CONFIG_HOME set", - args: "env", - envvars: map[string]string{helmpath.ConfigHomeEnvVar: "/foo/helm"}, - configPath: "/foo/helm", - }, - { - name: "with $HELM_DATA_HOME set", - args: "env", - envvars: map[string]string{helmpath.DataHomeEnvVar: "/foo/helm"}, - dataPath: "/foo/helm", - }, } for _, tt := range tests { @@ -80,7 +65,7 @@ func TestRootCmd(t *testing.T) { ensure.HelmHome(t) for k, v := range tt.envvars { - os.Setenv(k, v) + t.Setenv(k, v) } if _, _, err := executeActionCommand(tt.args); err != nil { @@ -129,3 +114,20 @@ func TestUnknownSubCmd(t *testing.T) { // func TestRootFileCompletion(t *testing.T) { // checkFileCompletion(t, "", false) // } + +func TestRootCmdLogger(t *testing.T) { + args := []string{} + buf := new(bytes.Buffer) + actionConfig := action.NewConfiguration() + _, err := newRootCmdWithConfig(actionConfig, buf, args, SetupLogging) + if err != nil { + t.Errorf("expected no error, got: '%v'", err) + } + + l1 := actionConfig.Logger() + l2 := slog.Default() + + if l1.Handler() != l2.Handler() { + t.Error("expected actionConfig logger to be the slog default logger") + } +} diff --git a/pkg/helm/pkg/cmd/search.go b/pkg/helm/pkg/cmd/search.go new file mode 100644 index 00000000..4d110286 --- /dev/null +++ b/pkg/helm/pkg/cmd/search.go @@ -0,0 +1,43 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "io" + + "github.com/spf13/cobra" +) + +const searchDesc = ` +Search provides the ability to search for Helm charts in the various places +they can be stored including the Artifact Hub and repositories you have added. +Use search subcommands to search different locations for charts. +` + +func newSearchCmd(out io.Writer) *cobra.Command { + + cmd := &cobra.Command{ + Use: "search [keyword]", + Short: "search for a keyword in charts", + Long: searchDesc, + } + + cmd.AddCommand(newSearchHubCmd(out)) + cmd.AddCommand(newSearchRepoCmd(out)) + + return cmd +} diff --git a/pkg/helm/pkg/cmd/search/search.go b/pkg/helm/pkg/cmd/search/search.go new file mode 100644 index 00000000..c89a220f --- /dev/null +++ b/pkg/helm/pkg/cmd/search/search.go @@ -0,0 +1,227 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Package search provides client-side repository searching. + +This supports building an in-memory search index based on the contents of +multiple repositories, and then using string matching or regular expressions +to find matches. +*/ +package search + +import ( + "path" + "regexp" + "sort" + "strings" + + "github.com/Masterminds/semver/v3" + + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +// Result is a search result. +// +// Score indicates how close it is to match. The higher the score, the longer +// the distance. +type Result struct { + Name string + Score int + Chart *repo.ChartVersion +} + +// Index is a searchable index of chart information. +type Index struct { + lines map[string]string + charts map[string]*repo.ChartVersion +} + +const sep = "\v" + +// NewIndex creates a new Index. +func NewIndex() *Index { + return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} +} + +// verSep is a separator for version fields in map keys. +const verSep = "$$" + +// AddRepo adds a repository index to the search index. +func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) { + ind.SortEntries() + for name, ref := range ind.Entries { + if len(ref) == 0 { + // Skip chart names that have zero releases. + continue + } + // By convention, an index file is supposed to have the newest at the + // 0 slot, so our best bet is to grab the 0 entry and build the index + // entry off of that. + // Note: Do not use filePath.Join since on Windows it will return \ + // which results in a repo name that cannot be understood. + fname := path.Join(rname, name) + if !all { + i.lines[fname] = indstr(rname, ref[0]) + i.charts[fname] = ref[0] + continue + } + + // If 'all' is set, then we go through all of the refs, and add them all + // to the index. This will generate a lot of near-duplicate entries. + for _, rr := range ref { + versionedName := fname + verSep + rr.Version + i.lines[versionedName] = indstr(rname, rr) + i.charts[versionedName] = rr + } + } +} + +// All returns all charts in the index as if they were search results. +// +// Each will be given a score of 0. +func (i *Index) All() []*Result { + res := make([]*Result, len(i.charts)) + j := 0 + for name, ch := range i.charts { + parts := strings.Split(name, verSep) + res[j] = &Result{ + Name: parts[0], + Chart: ch, + } + j++ + } + return res +} + +// Search searches an index for the given term. +// +// Threshold indicates the maximum score a term may have before being marked +// irrelevant. (Low score means higher relevance. Golf, not bowling.) +// +// If regexp is true, the term is treated as a regular expression. Otherwise, +// term is treated as a literal string. +func (i *Index) Search(term string, threshold int, regexp bool) ([]*Result, error) { + if regexp { + return i.SearchRegexp(term, threshold) + } + return i.SearchLiteral(term, threshold), nil +} + +// calcScore calculates a score for a match. +func (i *Index) calcScore(index int, matchline string) int { + + // This is currently tied to the fact that sep is a single char. + splits := []int{} + s := rune(sep[0]) + for i, ch := range matchline { + if ch == s { + splits = append(splits, i) + } + } + + for i, pos := range splits { + if index > pos { + continue + } + return i + } + return len(splits) +} + +// SearchLiteral does a literal string search (no regexp). +func (i *Index) SearchLiteral(term string, threshold int) []*Result { + term = strings.ToLower(term) + buf := []*Result{} + for k, v := range i.lines { + lv := strings.ToLower(v) + res := strings.Index(lv, term) + if score := i.calcScore(res, lv); res != -1 && score < threshold { + parts := strings.Split(k, verSep) // Remove version, if it is there. + buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) + } + } + return buf +} + +// SearchRegexp searches using a regular expression. +func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) { + matcher, err := regexp.Compile(re) + if err != nil { + return []*Result{}, err + } + buf := []*Result{} + for k, v := range i.lines { + ind := matcher.FindStringIndex(v) + if len(ind) == 0 { + continue + } + if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { + parts := strings.Split(k, verSep) // Remove version, if it is there. + buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]}) + } + } + return buf, nil +} + +// SortScore does an in-place sort of the results. +// +// Lowest scores are highest on the list. Matching scores are subsorted alphabetically. +func SortScore(r []*Result) { + sort.Sort(scoreSorter(r)) +} + +// scoreSorter sorts results by score, and subsorts by alpha Name. +type scoreSorter []*Result + +// Len returns the length of this scoreSorter. +func (s scoreSorter) Len() int { return len(s) } + +// Swap performs an in-place swap. +func (s scoreSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Less compares a to b, and returns true if a is less than b. +func (s scoreSorter) Less(a, b int) bool { + first := s[a] + second := s[b] + + if first.Score > second.Score { + return false + } + if first.Score < second.Score { + return true + } + if first.Name == second.Name { + v1, err := semver.NewVersion(first.Chart.Version) + if err != nil { + return true + } + v2, err := semver.NewVersion(second.Chart.Version) + if err != nil { + return true + } + // Sort so that the newest chart is higher than the oldest chart. This is + // the opposite of what you'd expect in a function called Less. + return v1.GreaterThan(v2) + } + return first.Name < second.Name +} + +func indstr(name string, ref *repo.ChartVersion) string { + i := ref.Name + sep + name + "/" + ref.Name + sep + + ref.Description + sep + strings.Join(ref.Keywords, " ") + return i +} diff --git a/pkg/helm/pkg/cmd/search/search_test.go b/pkg/helm/pkg/cmd/search/search_test.go new file mode 100644 index 00000000..1c44b9e0 --- /dev/null +++ b/pkg/helm/pkg/cmd/search/search_test.go @@ -0,0 +1,311 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package search + +import ( + "strings" + "testing" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +func TestSortScore(t *testing.T) { + in := []*Result{ + {Name: "bbb", Score: 0, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, + {Name: "aaa", Score: 5}, + {Name: "abb", Score: 5}, + {Name: "aab", Score: 0}, + {Name: "bab", Score: 5}, + {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.4"}}}, + {Name: "ver", Score: 5, Chart: &repo.ChartVersion{Metadata: &chart.Metadata{Version: "1.2.3"}}}, + } + expect := []string{"aab", "bbb", "aaa", "abb", "bab", "ver", "ver"} + expectScore := []int{0, 0, 5, 5, 5, 5, 5} + SortScore(in) + + // Test Score + for i := range expectScore { + if expectScore[i] != in[i].Score { + t.Errorf("Sort error on index %d: expected %d, got %d", i, expectScore[i], in[i].Score) + } + } + // Test Name + for i := range expect { + if expect[i] != in[i].Name { + t.Errorf("Sort error: expected %s, got %s", expect[i], in[i].Name) + } + } + + // Test version of last two items + if in[5].Chart.Version != "1.2.4" { + t.Errorf("Expected 1.2.4, got %s", in[5].Chart.Version) + } + if in[6].Chart.Version != "1.2.3" { + t.Error("Expected 1.2.3 to be last") + } +} + +var indexfileEntries = map[string]repo.ChartVersions{ + "niña": { + { + URLs: []string{"http://example.com/charts/nina-0.1.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "niña", + Version: "0.1.0", + Description: "One boat", + }, + }, + }, + "pinta": { + { + URLs: []string{"http://example.com/charts/pinta-0.1.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "pinta", + Version: "0.1.0", + Description: "Two ship", + }, + }, + }, + "santa-maria": { + { + URLs: []string{"http://example.com/charts/santa-maria-1.2.3.tgz"}, + Metadata: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.3", + Description: "Three boat", + }, + }, + { + URLs: []string{"http://example.com/charts/santa-maria-1.2.2-rc-1.tgz"}, + Metadata: &chart.Metadata{ + Name: "santa-maria", + Version: "1.2.2-RC-1", + Description: "Three boat", + }, + }, + }, +} + +func loadTestIndex(_ *testing.T, all bool) *Index { + i := NewIndex() + i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all) + i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ + "Pinta": { + { + URLs: []string{"http://example.com/charts/pinta-2.0.0.tgz"}, + Metadata: &chart.Metadata{ + Name: "Pinta", + Version: "2.0.0", + Description: "Two ship, version two", + }, + }, + }, + }}, all) + return i +} + +func TestAll(t *testing.T) { + i := loadTestIndex(t, false) + all := i.All() + if len(all) != 4 { + t.Errorf("Expected 4 entries, got %d", len(all)) + } + + i = loadTestIndex(t, true) + all = i.All() + if len(all) != 5 { + t.Errorf("Expected 5 entries, got %d", len(all)) + } +} + +func TestAddRepo_Sort(t *testing.T) { + i := loadTestIndex(t, true) + sr, err := i.Search("TESTING/SANTA-MARIA", 100, false) + if err != nil { + t.Fatal(err) + } + SortScore(sr) + + ch := sr[0] + expect := "1.2.3" + if ch.Chart.Version != expect { + t.Errorf("Expected %q, got %q", expect, ch.Chart.Version) + } +} + +func TestSearchByName(t *testing.T) { + + tests := []struct { + name string + query string + expect []*Result + regexp bool + fail bool + failMsg string + }{ + { + name: "basic search for one result", + query: "santa-maria", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "basic search for two results", + query: "pinta", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/Pinta"}, + }, + }, + { + name: "repo-specific search for one result", + query: "ztesting/pinta", + expect: []*Result{ + {Name: "ztesting/Pinta"}, + }, + }, + { + name: "partial name search", + query: "santa", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "description search, one result", + query: "Three", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + }, + { + name: "description search, two results", + query: "two", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/Pinta"}, + }, + }, + { + name: "search mixedCase and result should be mixedCase too", + query: "pinta", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/Pinta"}, + }, + }, + { + name: "description upper search, two results", + query: "TWO", + expect: []*Result{ + {Name: "testing/pinta"}, + {Name: "ztesting/Pinta"}, + }, + }, + { + name: "nothing found", + query: "mayflower", + expect: []*Result{}, + }, + { + name: "regexp, one result", + query: "Th[ref]*", + expect: []*Result{ + {Name: "testing/santa-maria"}, + }, + regexp: true, + }, + { + name: "regexp, fail compile", + query: "th[", + expect: []*Result{}, + regexp: true, + fail: true, + failMsg: "error parsing regexp:", + }, + } + + i := loadTestIndex(t, false) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + charts, err := i.Search(tt.query, 100, tt.regexp) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.failMsg) { + t.Fatalf("Unexpected error message: %s", err) + } + return + } + t.Fatalf("%s: %s", tt.name, err) + } + // Give us predictably ordered results. + SortScore(charts) + + l := len(charts) + if l != len(tt.expect) { + t.Fatalf("Expected %d result, got %d", len(tt.expect), l) + } + // For empty result sets, just keep going. + if l == 0 { + return + } + + for i, got := range charts { + ex := tt.expect[i] + if got.Name != ex.Name { + t.Errorf("[%d]: Expected name %q, got %q", i, ex.Name, got.Name) + } + } + + }) + } +} + +func TestSearchByNameAll(t *testing.T) { + // Test with the All bit turned on. + i := loadTestIndex(t, true) + cs, err := i.Search("santa-maria", 100, false) + if err != nil { + t.Fatal(err) + } + if len(cs) != 2 { + t.Errorf("expected 2 charts, got %d", len(cs)) + } +} + +func TestCalcScore(t *testing.T) { + i := NewIndex() + + fields := []string{"aaa", "bbb", "ccc", "ddd"} + matchline := strings.Join(fields, sep) + if r := i.calcScore(2, matchline); r != 0 { + t.Errorf("Expected 0, got %d", r) + } + if r := i.calcScore(5, matchline); r != 1 { + t.Errorf("Expected 1, got %d", r) + } + if r := i.calcScore(10, matchline); r != 2 { + t.Errorf("Expected 2, got %d", r) + } + if r := i.calcScore(14, matchline); r != 3 { + t.Errorf("Expected 3, got %d", r) + } +} diff --git a/pkg/helm/cmd/helm/search_hub.go b/pkg/helm/pkg/cmd/search_hub.go similarity index 94% rename from pkg/helm/cmd/helm/search_hub.go rename to pkg/helm/pkg/cmd/search_hub.go index 6674c5a6..6f327743 100644 --- a/pkg/helm/cmd/helm/search_hub.go +++ b/pkg/helm/pkg/cmd/search_hub.go @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" "io" + "log/slog" "strings" "github.com/gosuri/uitable" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/werf/nelm/pkg/helm/intern/monocular" @@ -64,7 +64,7 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { Use: "hub [KEYWORD]", Short: "search for charts in the Artifact Hub or your own hub instance", Long: searchHubDesc, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { return o.run(out, args) }, } @@ -83,13 +83,13 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { func (o *searchHubOptions) run(out io.Writer, args []string) error { c, err := monocular.New(o.searchEndpoint) if err != nil { - return errors.Wrap(err, fmt.Sprintf("unable to create connection to %q", o.searchEndpoint)) + return fmt.Errorf("unable to create connection to %q: %w", o.searchEndpoint, err) } q := strings.Join(args, " ") results, err := c.Search(q) if err != nil { - debug("%s", err) + slog.Debug("search failed", slog.Any("error", err)) return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) } @@ -190,9 +190,10 @@ func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) er return output.EncodeJSON(out, chartList) case output.YAML: return output.EncodeYAML(out, chartList) + default: + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil } - // Because this is a non-exported function and only called internally by - // WriteJSON and WriteYAML, we shouldn't get invalid types - return nil } diff --git a/pkg/helm/cmd/helm/search_hub_test.go b/pkg/helm/pkg/cmd/search_hub_test.go similarity index 99% rename from pkg/helm/cmd/helm/search_hub_test.go rename to pkg/helm/pkg/cmd/search_hub_test.go index 5c8951b0..8e056f77 100644 --- a/pkg/helm/cmd/helm/search_hub_test.go +++ b/pkg/helm/pkg/cmd/search_hub_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" @@ -27,7 +27,7 @@ func TestSearchHubCmd(t *testing.T) { // Setup a mock search service var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, searchResult) })) defer ts.Close() @@ -57,7 +57,7 @@ func TestSearchHubListRepoCmd(t *testing.T) { // Setup a mock search service var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://charts.helm.sh/stable"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://charts.helm.sh/stable/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}` - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, searchResult) })) defer ts.Close() @@ -155,7 +155,7 @@ func TestSearchHubCmd_FailOnNoResponseTests(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Setup a mock search service - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { fmt.Fprintln(w, tt.response) })) defer ts.Close() diff --git a/pkg/helm/cmd/helm/search_repo.go b/pkg/helm/pkg/cmd/search_repo.go similarity index 94% rename from pkg/helm/cmd/helm/search_repo.go rename to pkg/helm/pkg/cmd/search_repo.go index aee4f50e..b386757f 100644 --- a/pkg/helm/cmd/helm/search_repo.go +++ b/pkg/helm/pkg/cmd/search_repo.go @@ -14,26 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "bufio" "bytes" + "errors" "fmt" "io" + "log/slog" "os" "path/filepath" "strings" "github.com/Masterminds/semver/v3" "github.com/gosuri/uitable" - "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/search" "github.com/werf/nelm/pkg/helm/pkg/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/cmd/search" "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/repo" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) const searchRepoDesc = ` @@ -81,7 +82,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command { Use: "repo [keyword]", Short: "search repositories for a keyword in charts", Long: searchRepoDesc, - RunE: func(cmd *cobra.Command, args []string) error { + RunE: func(_ *cobra.Command, args []string) error { o.repoFile = settings.RepositoryConfig o.repoCacheDir = settings.RepositoryCache return o.run(out, args) @@ -130,17 +131,17 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error { } func (o *searchRepoOptions) setupSearchedVersion() { - debug("Original chart version: %q", o.version) + slog.Debug("original chart version", "version", o.version) if o.version != "" { return } if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases). - debug("setting version to >0.0.0-0") + slog.Debug("setting version to >0.0.0-0") o.version = ">0.0.0-0" - } else { // search only for stable releases, prerelease versions will be skip - debug("setting version to >0.0.0") + } else { // search only for stable releases, prerelease versions will be skipped + slog.Debug("setting version to >0.0.0") o.version = ">0.0.0" } } @@ -152,7 +153,7 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res constraint, err := semver.NewConstraint(o.version) if err != nil { - return res, errors.Wrap(err, "an invalid version/constraint format") + return res, fmt.Errorf("an invalid version/constraint format: %w", err) } data := res[:0] @@ -189,8 +190,7 @@ func (o *searchRepoOptions) buildIndex() (*search.Index, error) { f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n)) ind, err := repo.LoadIndexFile(f) if err != nil { - warning("Repo %q is corrupt or missing. Try 'helm repo update'.", n) - warning("%s", err) + slog.Warn("repo is corrupt or missing", slog.String("repo", n), slog.Any("error", err)) continue } @@ -260,11 +260,11 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) e return output.EncodeJSON(out, chartList) case output.YAML: return output.EncodeYAML(out, chartList) + default: + // Because this is a non-exported function and only called internally by + // WriteJSON and WriteYAML, we shouldn't get invalid types + return nil } - - // Because this is a non-exported function and only called internally by - // WriteJSON and WriteYAML, we shouldn't get invalid types - return nil } // Provides the list of charts that are part of the specified repo, and that starts with 'prefix'. @@ -287,7 +287,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string { if isNotExist(err) { // If there is no cached charts file, fallback to the full index file. // This is much slower but can happen after the caching feature is first - // installed but before the user does a 'helm repo update' to generate the + // installed but before the user does a 'helm repo update' to generate the // first cached charts file. path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) if indexFile, err := repo.LoadIndexFile(path); err == nil { diff --git a/pkg/helm/cmd/helm/search_repo_test.go b/pkg/helm/pkg/cmd/search_repo_test.go similarity index 99% rename from pkg/helm/cmd/helm/search_repo_test.go rename to pkg/helm/pkg/cmd/search_repo_test.go index 6bd11bb3..e7f104e0 100644 --- a/pkg/helm/cmd/helm/search_repo_test.go +++ b/pkg/helm/pkg/cmd/search_repo_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "testing" diff --git a/pkg/helm/pkg/cmd/search_test.go b/pkg/helm/pkg/cmd/search_test.go new file mode 100644 index 00000000..a0e5d84c --- /dev/null +++ b/pkg/helm/pkg/cmd/search_test.go @@ -0,0 +1,23 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import "testing" + +func TestSearchFileCompletion(t *testing.T) { + checkFileCompletion(t, "search", false) +} diff --git a/pkg/helm/pkg/cmd/show.go b/pkg/helm/pkg/cmd/show.go new file mode 100644 index 00000000..7e458805 --- /dev/null +++ b/pkg/helm/pkg/cmd/show.go @@ -0,0 +1,236 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + "log" + "log/slog" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" +) + +const showDesc = ` +This command consists of multiple subcommands to display information about a chart +` + +const showAllDesc = ` +This command inspects a chart (directory, file, or URL) and displays all its content +(values.yaml, Chart.yaml, README) +` + +const showValuesDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the values.yaml file +` + +const showChartDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the Chart.yaml file +` + +const readmeChartDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the README file +` + +const showCRDsDesc = ` +This command inspects a chart (directory, file, or URL) and displays the contents +of the CustomResourceDefinition files +` + +func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewShow(action.ShowAll, cfg) + + showCommand := &cobra.Command{ + Use: "show", + Short: "show information of a chart", + Aliases: []string{"inspect"}, + Long: showDesc, + Args: require.NoArgs, + } + + // Function providing dynamic auto-completion + validArgsFunc := func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return noMoreArgsComp() + } + return compListCharts(toComplete, true) + } + + all := &cobra.Command{ + Use: "all [CHART]", + Short: "show all information of the chart", + Long: showAllDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(_ *cobra.Command, args []string) error { + client.OutputFormat = action.ShowAll + err := addRegistryClient(client) + if err != nil { + return err + } + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + valuesSubCmd := &cobra.Command{ + Use: "values [CHART]", + Short: "show the chart's values", + Long: showValuesDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(_ *cobra.Command, args []string) error { + client.OutputFormat = action.ShowValues + err := addRegistryClient(client) + if err != nil { + return err + } + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + chartSubCmd := &cobra.Command{ + Use: "chart [CHART]", + Short: "show the chart's definition", + Long: showChartDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(_ *cobra.Command, args []string) error { + client.OutputFormat = action.ShowChart + err := addRegistryClient(client) + if err != nil { + return err + } + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + readmeSubCmd := &cobra.Command{ + Use: "readme [CHART]", + Short: "show the chart's README", + Long: readmeChartDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(_ *cobra.Command, args []string) error { + client.OutputFormat = action.ShowReadme + err := addRegistryClient(client) + if err != nil { + return err + } + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + crdsSubCmd := &cobra.Command{ + Use: "crds [CHART]", + Short: "show the chart's CRDs", + Long: showCRDsDesc, + Args: require.ExactArgs(1), + ValidArgsFunction: validArgsFunc, + RunE: func(_ *cobra.Command, args []string) error { + client.OutputFormat = action.ShowCRDs + err := addRegistryClient(client) + if err != nil { + return err + } + output, err := runShow(args, client) + if err != nil { + return err + } + fmt.Fprint(out, output) + return nil + }, + } + + cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd, crdsSubCmd} + for _, subCmd := range cmds { + addShowFlags(subCmd, client) + showCommand.AddCommand(subCmd) + } + + return showCommand +} + +func addShowFlags(subCmd *cobra.Command, client *action.Show) { + f := subCmd.Flags() + + f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") + if subCmd.Name() == "values" { + f.StringVar(&client.JSONPathTemplate, "jsonpath", "", "supply a JSONPath expression to filter the output") + } + addChartPathOptionsFlags(f, &client.ChartPathOptions) + + err := subCmd.RegisterFlagCompletionFunc("version", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 1 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return compVersionFlag(args[0], toComplete) + }) + + if err != nil { + log.Fatal(err) + } +} + +func runShow(args []string, client *action.Show) (string, error) { + slog.Debug("original chart version", "version", client.Version) + if client.Version == "" && client.Devel { + slog.Debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" + } + + cp, err := client.LocateChart(args[0], settings) + if err != nil { + return "", err + } + return client.Run(cp) +} + +func addRegistryClient(client *action.Show) error { + registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, + client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) + if err != nil { + return fmt.Errorf("missing registry client: %w", err) + } + client.SetRegistryClient(registryClient) + return nil +} diff --git a/pkg/helm/pkg/cmd/show_test.go b/pkg/helm/pkg/cmd/show_test.go new file mode 100644 index 00000000..7f773ce8 --- /dev/null +++ b/pkg/helm/pkg/cmd/show_test.go @@ -0,0 +1,158 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "path/filepath" + "strings" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" +) + +func TestShowPreReleaseChart(t *testing.T) { + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"), + ) + defer srv.Stop() + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + args string + flags string + fail bool + expectedErr string + }{ + { + name: "show pre-release chart", + args: "test/pre-release-chart", + fail: true, + expectedErr: "chart \"pre-release-chart\" matching not found in test index. (try 'helm repo update'): no chart version found for pre-release-chart-", + }, + { + name: "show pre-release chart", + args: "test/pre-release-chart", + fail: true, + flags: "--version 1.0.0", + expectedErr: "chart \"pre-release-chart\" matching 1.0.0 not found in test index. (try 'helm repo update'): no chart version found for pre-release-chart-1.0.0", + }, + { + name: "show pre-release chart with 'devel' flag", + args: "test/pre-release-chart", + flags: "--devel", + fail: false, + }, + } + + contentTmp := t.TempDir() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Root() + cmd := fmt.Sprintf("show all '%s' %s --repository-config %s --repository-cache %s --content-cache %s", + tt.args, + tt.flags, + filepath.Join(outdir, "repositories.yaml"), + outdir, + contentTmp, + ) + //_, out, err := executeActionCommand(cmd) + _, _, err := executeActionCommand(cmd) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("%q expected error: %s, got: %s", tt.name, tt.expectedErr, err.Error()) + } + return + } + t.Errorf("%q reported error: %s", tt.name, err) + } + }) + } +} + +func TestShowVersionCompletion(t *testing.T) { + repoFile := "testdata/helmhome/helm/repositories.yaml" + repoCache := "testdata/helmhome/helm/repository" + + repoSetup := fmt.Sprintf("--repository-config %s --repository-cache %s", repoFile, repoCache) + + tests := []cmdTestCase{{ + name: "completion for show version flag", + cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag, no filter", + cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version 0.3", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag too few args", + cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for show version flag too many args", + cmd: fmt.Sprintf("%s __complete show chart testing/alpine badarg --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for show version flag invalid chart", + cmd: fmt.Sprintf("%s __complete show chart invalid/invalid --version ''", repoSetup), + golden: "output/version-invalid-comp.txt", + }, { + name: "completion for show version flag with all", + cmd: fmt.Sprintf("%s __complete show all testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag with readme", + cmd: fmt.Sprintf("%s __complete show readme testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }, { + name: "completion for show version flag with values", + cmd: fmt.Sprintf("%s __complete show values testing/alpine --version ''", repoSetup), + golden: "output/version-comp.txt", + }} + runTestCmd(t, tests) +} + +func TestShowFileCompletion(t *testing.T) { + checkFileCompletion(t, "show", false) +} + +func TestShowAllFileCompletion(t *testing.T) { + checkFileCompletion(t, "show all", true) +} + +func TestShowChartFileCompletion(t *testing.T) { + checkFileCompletion(t, "show chart", true) +} + +func TestShowReadmeFileCompletion(t *testing.T) { + checkFileCompletion(t, "show readme", true) +} + +func TestShowValuesFileCompletion(t *testing.T) { + checkFileCompletion(t, "show values", true) +} + +func TestShowCRDsFileCompletion(t *testing.T) { + checkFileCompletion(t, "show crds", true) +} diff --git a/pkg/helm/pkg/cmd/status.go b/pkg/helm/pkg/cmd/status.go new file mode 100644 index 00000000..6879711e --- /dev/null +++ b/pkg/helm/pkg/cmd/status.go @@ -0,0 +1,257 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "bytes" + "fmt" + "io" + "log" + "strings" + "time" + + "github.com/spf13/cobra" + + "k8s.io/kubectl/pkg/cmd/get" + + coloroutput "github.com/werf/nelm/pkg/helm/intern/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" + "github.com/werf/nelm/pkg/helm/pkg/cli/output" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" + "github.com/werf/nelm/pkg/helm/pkg/release" + releasev1 "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +// NOTE: Keep the list of statuses up-to-date with pkg/release/status.go. +var statusHelp = ` +This command shows the status of a named release. +The status consists of: +- last deployment time +- k8s namespace in which the release lives +- state of the release (can be: unknown, deployed, uninstalled, superseded, failed, uninstalling, pending-install, pending-upgrade or pending-rollback) +- revision of the release +- description of the release (can be completion message or error message) +- list of resources that this release consists of +- details on last test suite run, if applicable +- additional notes provided by the chart +` + +func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { + client := action.NewStatus(cfg) + var outfmt output.Format + + cmd := &cobra.Command{ + Use: "status RELEASE_NAME", + Short: "display the status of the named release", + Long: statusHelp, + Args: require.ExactArgs(1), + ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 0 { + return noMoreArgsComp() + } + return compListReleases(toComplete, args, cfg) + }, + RunE: func(_ *cobra.Command, args []string) error { + // When the output format is a table the resources should be fetched + // and displayed as a table. When YAML or JSON the resources will be + // returned. This mirrors the handling in kubectl. + if outfmt == output.Table { + client.ShowResourcesTable = true + } + reli, err := client.Run(args[0]) + if err != nil { + return err + } + rel, err := releaserToV1Release(reli) + if err != nil { + return err + } + + // strip chart metadata from the output + rel.Chart = nil + + return outfmt.Write(out, &statusPrinter{ + release: rel, + debug: false, + showMetadata: false, + hideNotes: false, + noColor: settings.ShouldDisableColor(), + }) + }, + } + + f := cmd.Flags() + + f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision") + + err := cmd.RegisterFlagCompletionFunc("revision", func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 1 { + return compListRevisions(toComplete, cfg, args[0]) + } + return nil, cobra.ShellCompDirectiveNoFileComp + }) + if err != nil { + log.Fatal(err) + } + + bindOutputFlag(cmd, &outfmt) + + return cmd +} + +type statusPrinter struct { + release release.Releaser + debug bool + showMetadata bool + hideNotes bool + noColor bool +} + +func (s statusPrinter) getV1Release() *releasev1.Release { + switch rel := s.release.(type) { + case releasev1.Release: + return &rel + case *releasev1.Release: + return rel + } + return &releasev1.Release{} +} + +func (s statusPrinter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, s.getV1Release()) +} + +func (s statusPrinter) WriteYAML(out io.Writer) error { + return output.EncodeYAML(out, s.getV1Release()) +} + +func (s statusPrinter) WriteTable(out io.Writer) error { + if s.release == nil { + return nil + } + rel := s.getV1Release() + _, _ = fmt.Fprintf(out, "NAME: %s\n", rel.Name) + if !rel.Info.LastDeployed.IsZero() { + _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Format(time.ANSIC)) + } + _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(rel.Namespace, s.noColor)) + _, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(rel.Info.Status, s.noColor)) + _, _ = fmt.Fprintf(out, "REVISION: %d\n", rel.Version) + if s.showMetadata { + _, _ = fmt.Fprintf(out, "CHART: %s\n", rel.Chart.Metadata.Name) + _, _ = fmt.Fprintf(out, "VERSION: %s\n", rel.Chart.Metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", rel.Chart.Metadata.AppVersion) + } + _, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", rel.Info.Description) + + if len(rel.Info.Resources) > 0 { + buf := new(bytes.Buffer) + printFlags := get.NewHumanPrintFlags() + typePrinter, _ := printFlags.ToPrinter("") + printer := &get.TablePrinter{Delegate: typePrinter} + + var keys []string + for key := range rel.Info.Resources { + keys = append(keys, key) + } + + for _, t := range keys { + _, _ = fmt.Fprintf(buf, "==> %s\n", t) + + vk := rel.Info.Resources[t] + for _, resource := range vk { + if err := printer.PrintObj(resource, buf); err != nil { + _, _ = fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err) + } + } + + buf.WriteString("\n") + } + + _, _ = fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String()) + } + + executions := executionsByHookEvent(rel) + if tests, ok := executions[releasev1.HookTest]; !ok || len(tests) == 0 { + _, _ = fmt.Fprintln(out, "TEST SUITE: None") + } else { + for _, h := range tests { + // Don't print anything if hook has not been initiated + if h.LastRun.StartedAt.IsZero() { + continue + } + _, _ = fmt.Fprintf(out, "TEST SUITE: %s\n%s\n%s\n%s\n", + h.Name, + fmt.Sprintf("Last Started: %s", h.LastRun.StartedAt.Format(time.ANSIC)), + fmt.Sprintf("Last Completed: %s", h.LastRun.CompletedAt.Format(time.ANSIC)), + fmt.Sprintf("Phase: %s", h.LastRun.Phase), + ) + } + } + + if s.debug { + _, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:") + err := output.EncodeYAML(out, rel.Config) + if err != nil { + return err + } + // Print an extra newline + _, _ = fmt.Fprintln(out) + + cfg, err := util.CoalesceValues(rel.Chart, rel.Config) + if err != nil { + return err + } + + _, _ = fmt.Fprintln(out, "COMPUTED VALUES:") + err = output.EncodeYAML(out, cfg.AsMap()) + if err != nil { + return err + } + // Print an extra newline + _, _ = fmt.Fprintln(out) + } + + if strings.EqualFold(rel.Info.Description, "Dry run complete") || s.debug { + _, _ = fmt.Fprintln(out, "HOOKS:") + for _, h := range rel.Hooks { + _, _ = fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) + } + _, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", rel.Manifest) + } + + // Hide notes from output - option in install and upgrades + if !s.hideNotes && len(rel.Info.Notes) > 0 { + _, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(rel.Info.Notes)) + } + return nil +} + +func executionsByHookEvent(rel *releasev1.Release) map[releasev1.HookEvent][]*releasev1.Hook { + result := make(map[releasev1.HookEvent][]*releasev1.Hook) + for _, h := range rel.Hooks { + for _, e := range h.Events { + executions, ok := result[e] + if !ok { + executions = []*releasev1.Hook{} + } + result[e] = append(executions, h) + } + } + return result +} diff --git a/pkg/helm/pkg/cmd/status_test.go b/pkg/helm/pkg/cmd/status_test.go new file mode 100644 index 00000000..d0635d54 --- /dev/null +++ b/pkg/helm/pkg/cmd/status_test.go @@ -0,0 +1,220 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" + "time" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +func TestStatusCmd(t *testing.T) { + releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { + info.LastDeployed = time.Unix(1452902400, 0).UTC() + return []*release.Release{{ + Name: "flummoxed-chickadee", + Namespace: "default", + Info: info, + Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "name", Version: "1.2.3", AppVersion: "3.2.1"}}, + Hooks: hooks, + }} + } + + tests := []cmdTestCase{{ + name: "get status of a deployed release", + cmd: "status flummoxed-chickadee", + golden: "output/status.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + }), + }, { + name: "get status of a deployed release, with desc", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-desc.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + Description: "Mock description", + }), + }, { + name: "get status of a deployed release with notes", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-notes.txt", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + Notes: "release notes", + }), + }, { + name: "get status of a deployed release with notes in json", + cmd: "status flummoxed-chickadee -o json", + golden: "output/status.json", + rels: releasesMockWithStatus(&release.Info{ + Status: common.StatusDeployed, + Notes: "release notes", + }), + }, { + name: "get status of a deployed release with resources", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-resources.txt", + rels: releasesMockWithStatus( + &release.Info{ + Status: common.StatusDeployed, + }, + ), + }, { + name: "get status of a deployed release with resources in json", + cmd: "status flummoxed-chickadee -o json", + golden: "output/status-with-resources.json", + rels: releasesMockWithStatus( + &release.Info{ + Status: common.StatusDeployed, + }, + ), + }, { + name: "get status of a deployed release with test suite", + cmd: "status flummoxed-chickadee", + golden: "output/status-with-test-suite.txt", + rels: releasesMockWithStatus( + &release.Info{ + Status: common.StatusDeployed, + }, + &release.Hook{ + Name: "never-run-test", + Events: []release.HookEvent{release.HookTest}, + }, + &release.Hook{ + Name: "passing-test", + Events: []release.HookEvent{release.HookTest}, + LastRun: release.HookExecution{ + StartedAt: mustParseTime("2006-01-02T15:04:05Z"), + CompletedAt: mustParseTime("2006-01-02T15:04:07Z"), + Phase: release.HookPhaseSucceeded, + }, + }, + &release.Hook{ + Name: "failing-test", + Events: []release.HookEvent{release.HookTest}, + LastRun: release.HookExecution{ + StartedAt: mustParseTime("2006-01-02T15:10:05Z"), + CompletedAt: mustParseTime("2006-01-02T15:10:07Z"), + Phase: release.HookPhaseFailed, + }, + }, + &release.Hook{ + Name: "passing-pre-install", + Events: []release.HookEvent{release.HookPreInstall}, + LastRun: release.HookExecution{ + StartedAt: mustParseTime("2006-01-02T15:00:05Z"), + CompletedAt: mustParseTime("2006-01-02T15:00:07Z"), + Phase: release.HookPhaseSucceeded, + }, + }, + ), + }} + runTestCmd(t, tests) +} + +func mustParseTime(t string) time.Time { + res, _ := time.Parse(time.RFC3339, t) + return res +} + +func TestStatusCompletion(t *testing.T) { + rels := []*release.Release{ + { + Name: "athos", + Namespace: "default", + Info: &release.Info{ + Status: common.StatusDeployed, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Athos-chart", + Version: "1.2.3", + }, + }, + }, { + Name: "porthos", + Namespace: "default", + Info: &release.Info{ + Status: common.StatusFailed, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Porthos-chart", + Version: "111.222.333", + }, + }, + }, { + Name: "aramis", + Namespace: "default", + Info: &release.Info{ + Status: common.StatusUninstalled, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Aramis-chart", + Version: "0.0.0", + }, + }, + }, { + Name: "dartagnan", + Namespace: "gascony", + Info: &release.Info{ + Status: common.StatusUnknown, + }, + Chart: &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "Dartagnan-chart", + Version: "1.2.3-prerelease", + }, + }, + }} + + tests := []cmdTestCase{{ + name: "completion for status", + cmd: "__complete status a", + golden: "output/status-comp.txt", + rels: rels, + }, { + name: "completion for status with too many arguments", + cmd: "__complete status dartagnan ''", + golden: "output/status-wrong-args-comp.txt", + rels: rels, + }, { + name: "completion for status with global flag", + cmd: "__complete status --debug a", + golden: "output/status-comp.txt", + rels: rels, + }} + runTestCmd(t, tests) +} + +func TestStatusRevisionCompletion(t *testing.T) { + revisionFlagCompletionTest(t, "status") +} + +func TestStatusOutputCompletion(t *testing.T) { + outputFlagCompletionTest(t, "status") +} + +func TestStatusFileCompletion(t *testing.T) { + checkFileCompletion(t, "status", false) + checkFileCompletion(t, "status myrelease", false) +} diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/completion.yaml b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/completion.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/completion.yaml rename to pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/completion.yaml diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/fullenv.sh b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/fullenv.sh similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm home with space/helm/plugins/fullenv/fullenv.sh rename to pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/fullenv.sh diff --git a/pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml new file mode 100644 index 00000000..a58544b0 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/plugins/fullenv/plugin.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +name: fullenv +type: cli/v1 +runtime: subprocess +config: + shortHelp: "show env vars" + longHelp: "show all env vars" + ignoreFlags: false +runtimeConfig: + platformCommand: + - command: "$HELM_PLUGIN_DIR/fullenv.sh" diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/repositories.yaml b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/repositories.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm home with space/helm/repositories.yaml rename to pkg/helm/pkg/cmd/testdata/helm home with space/helm/repositories.yaml diff --git a/pkg/helm/pkg/cmd/testdata/helm home with space/helm/repository/test-name-charts.txt b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/repository/test-name-charts.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/repository/test-name-index.yaml b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/repository/test-name-index.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm home with space/helm/repository/test-name-index.yaml rename to pkg/helm/pkg/cmd/testdata/helm home with space/helm/repository/test-name-index.yaml diff --git a/pkg/helm/cmd/helm/testdata/helm home with space/helm/repository/testing-index.yaml b/pkg/helm/pkg/cmd/testdata/helm home with space/helm/repository/testing-index.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm home with space/helm/repository/testing-index.yaml rename to pkg/helm/pkg/cmd/testdata/helm home with space/helm/repository/testing-index.yaml diff --git a/pkg/helm/cmd/helm/testdata/helm-test-key.pub b/pkg/helm/pkg/cmd/testdata/helm-test-key.pub similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm-test-key.pub rename to pkg/helm/pkg/cmd/testdata/helm-test-key.pub diff --git a/pkg/helm/cmd/helm/testdata/helm-test-key.secret b/pkg/helm/pkg/cmd/testdata/helm-test-key.secret similarity index 100% rename from pkg/helm/cmd/helm/testdata/helm-test-key.secret rename to pkg/helm/pkg/cmd/testdata/helm-test-key.secret diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/args.sh b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/args.sh new file mode 100755 index 00000000..6c62be8b --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/args.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +echo "$@" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.complete similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.complete diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml new file mode 100644 index 00000000..4156e7f1 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/args/plugin.yaml @@ -0,0 +1,11 @@ +name: args +type: cli/v1 +apiVersion: v1 +runtime: subprocess +config: + shortHelp: "echo args" + longHelp: "This echos args" + ignoreFlags: false +runtimeConfig: + platformCommand: + - command: "$HELM_PLUGIN_DIR/args.sh" diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/completion.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/completion.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.complete similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.complete diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml new file mode 100644 index 00000000..a0a0b525 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/echo/plugin.yaml @@ -0,0 +1,11 @@ +name: echo +type: cli/v1 +apiVersion: v1 +runtime: subprocess +config: + shortHelp: "echo stuff" + longHelp: "This echos stuff" + ignoreFlags: false +runtimeConfig: + platformCommand: + - command: "echo hello" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/completion.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/completion.yaml diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/exitwith.sh b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/exitwith.sh new file mode 100755 index 00000000..9cf68da6 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/exitwith.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env sh +exit "$1" diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml new file mode 100644 index 00000000..ba950825 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/exitwith/plugin.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +name: exitwith +type: cli/v1 +runtime: subprocess +config: + shortHelp: "exitwith code" + longHelp: "This exits with the specified exit code" + ignoreFlags: false +runtimeConfig: + platformCommand: + - command: "$HELM_PLUGIN_DIR/exitwith.sh" diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/completion.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/completion.yaml diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/fullenv.sh b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/fullenv.sh new file mode 100755 index 00000000..cc0c64a6 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/fullenv.sh @@ -0,0 +1,7 @@ +#!/bin/sh +echo HELM_PLUGIN_NAME=${HELM_PLUGIN_NAME} +echo HELM_PLUGIN_DIR=${HELM_PLUGIN_DIR} +echo HELM_PLUGINS=${HELM_PLUGINS} +echo HELM_REPOSITORY_CONFIG=${HELM_REPOSITORY_CONFIG} +echo HELM_REPOSITORY_CACHE=${HELM_REPOSITORY_CACHE} +echo HELM_BIN=${HELM_BIN} diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml new file mode 100644 index 00000000..a58544b0 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/fullenv/plugin.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +name: fullenv +type: cli/v1 +runtime: subprocess +config: + shortHelp: "show env vars" + longHelp: "show all env vars" + ignoreFlags: false +runtimeConfig: + platformCommand: + - command: "$HELM_PLUGIN_DIR/fullenv.sh" diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/plugin.yaml new file mode 100644 index 00000000..b6e8afa5 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/plugin.yaml @@ -0,0 +1,9 @@ +--- +apiVersion: v1 +name: "postrenderer-v1" +version: "1.2.3" +type: postrenderer/v1 +runtime: subprocess +runtimeConfig: + platformCommand: + - command: "${HELM_PLUGIN_DIR}/sed-test.sh" diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/sed-test.sh b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/sed-test.sh new file mode 100755 index 00000000..a016e398 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/postrenderer-v1/sed-test.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ $# -eq 0 ]; then + sed s/FOOTEST/BARTEST/g <&0 +else + sed s/FOOTEST/"$*"/g <&0 +fi diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/completion.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/completion.yaml new file mode 100644 index 00000000..027573ed --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/completion.yaml @@ -0,0 +1,13 @@ +name: shortenv +commands: + - name: list + flags: + - a + - all + - log + - name: remove + validArgs: + - all + - one +flags: +- global diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin-name.sh b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin-name.sh new file mode 100755 index 00000000..9e823ac1 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin-name.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +echo HELM_PLUGIN_NAME=${HELM_PLUGIN_NAME} diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml new file mode 100644 index 00000000..5fe053ed --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/helmhome/helm/plugins/shortenv/plugin.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +name: shortenv +type: cli/v1 +runtime: subprocess +config: + shortHelp: "env stuff" + longHelp: "show the env" + ignoreFlags: false +runtimeConfig: + platformCommand: + - command: ${HELM_PLUGIN_DIR}/plugin-name.sh diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/repositories.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/repositories.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/repositories.yaml rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/repositories.yaml diff --git a/pkg/helm/pkg/cmd/testdata/helmhome/helm/repository/test-name-charts.txt b/pkg/helm/pkg/cmd/testdata/helmhome/helm/repository/test-name-charts.txt new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/repository/test-name-index.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/repository/test-name-index.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/repository/test-name-index.yaml rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/repository/test-name-index.yaml diff --git a/pkg/helm/cmd/helm/testdata/helmhome/helm/repository/testing-index.yaml b/pkg/helm/pkg/cmd/testdata/helmhome/helm/repository/testing-index.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/helmhome/helm/repository/testing-index.yaml rename to pkg/helm/pkg/cmd/testdata/helmhome/helm/repository/testing-index.yaml diff --git a/pkg/helm/cmd/helm/testdata/output/chart-with-subchart-update.txt b/pkg/helm/pkg/cmd/testdata/output/chart-with-subchart-update.txt similarity index 80% rename from pkg/helm/cmd/helm/testdata/output/chart-with-subchart-update.txt rename to pkg/helm/pkg/cmd/testdata/output/chart-with-subchart-update.txt index dd8d3c35..5b2083e1 100644 --- a/pkg/helm/cmd/helm/testdata/output/chart-with-subchart-update.txt +++ b/pkg/helm/pkg/cmd/testdata/output/chart-with-subchart-update.txt @@ -1,10 +1,9 @@ NAME: updeps LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 NAMESPACE: default STATUS: deployed REVISION: 1 +DESCRIPTION: Install complete TEST SUITE: None NOTES: PARENT NOTES diff --git a/pkg/helm/cmd/helm/testdata/output/dependency-list-archive.txt b/pkg/helm/pkg/cmd/testdata/output/dependency-list-archive.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/dependency-list-archive.txt rename to pkg/helm/pkg/cmd/testdata/output/dependency-list-archive.txt diff --git a/pkg/helm/cmd/helm/testdata/output/dependency-list-no-chart-linux.txt b/pkg/helm/pkg/cmd/testdata/output/dependency-list-no-chart-linux.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/dependency-list-no-chart-linux.txt rename to pkg/helm/pkg/cmd/testdata/output/dependency-list-no-chart-linux.txt diff --git a/pkg/helm/cmd/helm/testdata/output/dependency-list-no-requirements-linux.txt b/pkg/helm/pkg/cmd/testdata/output/dependency-list-no-requirements-linux.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/dependency-list-no-requirements-linux.txt rename to pkg/helm/pkg/cmd/testdata/output/dependency-list-no-requirements-linux.txt diff --git a/pkg/helm/cmd/helm/testdata/output/dependency-list.txt b/pkg/helm/pkg/cmd/testdata/output/dependency-list.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/dependency-list.txt rename to pkg/helm/pkg/cmd/testdata/output/dependency-list.txt diff --git a/pkg/helm/cmd/helm/testdata/output/deprecated-chart.txt b/pkg/helm/pkg/cmd/testdata/output/deprecated-chart.txt similarity index 77% rename from pkg/helm/cmd/helm/testdata/output/deprecated-chart.txt rename to pkg/helm/pkg/cmd/testdata/output/deprecated-chart.txt index 7452b096..fcf5cc0e 100644 --- a/pkg/helm/cmd/helm/testdata/output/deprecated-chart.txt +++ b/pkg/helm/pkg/cmd/testdata/output/deprecated-chart.txt @@ -1,8 +1,7 @@ NAME: aeneas LAST DEPLOYED: Fri Sep 2 22:04:05 1977 -LAST PHASE: rollout -LAST STAGE: 0 NAMESPACE: default STATUS: deployed REVISION: 1 +DESCRIPTION: Install complete TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/docs-type-comp.txt b/pkg/helm/pkg/cmd/testdata/output/docs-type-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/docs-type-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/docs-type-comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/empty_default_comp.txt b/pkg/helm/pkg/cmd/testdata/output/empty_default_comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/empty_default_comp.txt rename to pkg/helm/pkg/cmd/testdata/output/empty_default_comp.txt diff --git a/pkg/helm/pkg/cmd/testdata/output/empty_nofile_comp.txt b/pkg/helm/pkg/cmd/testdata/output/empty_nofile_comp.txt new file mode 100644 index 00000000..3c537283 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/empty_nofile_comp.txt @@ -0,0 +1,3 @@ +_activeHelp_ This command does not take any more arguments (but may accept flags). +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/cmd/helm/testdata/output/env-comp.txt b/pkg/helm/pkg/cmd/testdata/output/env-comp.txt similarity index 95% rename from pkg/helm/cmd/helm/testdata/output/env-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/env-comp.txt index 8f9c53fc..9d38ee46 100644 --- a/pkg/helm/cmd/helm/testdata/output/env-comp.txt +++ b/pkg/helm/pkg/cmd/testdata/output/env-comp.txt @@ -2,6 +2,7 @@ HELM_BIN HELM_BURST_LIMIT HELM_CACHE_HOME HELM_CONFIG_HOME +HELM_CONTENT_CACHE HELM_DATA_HOME HELM_DEBUG HELM_KUBEAPISERVER diff --git a/pkg/helm/cmd/helm/testdata/output/history-limit.txt b/pkg/helm/pkg/cmd/testdata/output/history-limit.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/history-limit.txt rename to pkg/helm/pkg/cmd/testdata/output/history-limit.txt diff --git a/pkg/helm/cmd/helm/testdata/output/history.json b/pkg/helm/pkg/cmd/testdata/output/history.json similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/history.json rename to pkg/helm/pkg/cmd/testdata/output/history.json diff --git a/pkg/helm/cmd/helm/testdata/output/history.txt b/pkg/helm/pkg/cmd/testdata/output/history.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/history.txt rename to pkg/helm/pkg/cmd/testdata/output/history.txt diff --git a/pkg/helm/cmd/helm/testdata/output/history.yaml b/pkg/helm/pkg/cmd/testdata/output/history.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/history.yaml rename to pkg/helm/pkg/cmd/testdata/output/history.yaml diff --git a/pkg/helm/cmd/helm/testdata/output/issue-9027.txt b/pkg/helm/pkg/cmd/testdata/output/issue-9027.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/issue-9027.txt rename to pkg/helm/pkg/cmd/testdata/output/issue-9027.txt diff --git a/pkg/helm/cmd/helm/testdata/output/object-order.txt b/pkg/helm/pkg/cmd/testdata/output/object-order.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/object-order.txt rename to pkg/helm/pkg/cmd/testdata/output/object-order.txt diff --git a/pkg/helm/cmd/helm/testdata/output/output-comp.txt b/pkg/helm/pkg/cmd/testdata/output/output-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/output-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/output-comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/release_list_comp.txt b/pkg/helm/pkg/cmd/testdata/output/release_list_comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/release_list_comp.txt rename to pkg/helm/pkg/cmd/testdata/output/release_list_comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/release_list_repeat_comp.txt b/pkg/helm/pkg/cmd/testdata/output/release_list_repeat_comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/release_list_repeat_comp.txt rename to pkg/helm/pkg/cmd/testdata/output/release_list_repeat_comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/repo-add.txt b/pkg/helm/pkg/cmd/testdata/output/repo-add.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/repo-add.txt rename to pkg/helm/pkg/cmd/testdata/output/repo-add.txt diff --git a/pkg/helm/cmd/helm/testdata/output/repo-add2.txt b/pkg/helm/pkg/cmd/testdata/output/repo-add2.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/repo-add2.txt rename to pkg/helm/pkg/cmd/testdata/output/repo-add2.txt diff --git a/pkg/helm/pkg/cmd/testdata/output/repo-list-empty.txt b/pkg/helm/pkg/cmd/testdata/output/repo-list-empty.txt new file mode 100644 index 00000000..c6edb659 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/repo-list-empty.txt @@ -0,0 +1 @@ +no repositories to show diff --git a/pkg/helm/pkg/cmd/testdata/output/repo-list-no-headers.txt b/pkg/helm/pkg/cmd/testdata/output/repo-list-no-headers.txt new file mode 100644 index 00000000..13491aeb --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/repo-list-no-headers.txt @@ -0,0 +1,3 @@ +charts https://charts.helm.sh/stable +firstexample http://firstexample.com +secondexample http://secondexample.com diff --git a/pkg/helm/pkg/cmd/testdata/output/repo-list.txt b/pkg/helm/pkg/cmd/testdata/output/repo-list.txt new file mode 100644 index 00000000..edbd0ecc --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/repo-list.txt @@ -0,0 +1,4 @@ +NAME URL +charts https://charts.helm.sh/stable +firstexample http://firstexample.com +secondexample http://secondexample.com diff --git a/pkg/helm/cmd/helm/testdata/output/repo_list_comp.txt b/pkg/helm/pkg/cmd/testdata/output/repo_list_comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/repo_list_comp.txt rename to pkg/helm/pkg/cmd/testdata/output/repo_list_comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/repo_repeat_comp.txt b/pkg/helm/pkg/cmd/testdata/output/repo_repeat_comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/repo_repeat_comp.txt rename to pkg/helm/pkg/cmd/testdata/output/repo_repeat_comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/revision-comp.txt b/pkg/helm/pkg/cmd/testdata/output/revision-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/revision-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/revision-comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/revision-wrong-args-comp.txt b/pkg/helm/pkg/cmd/testdata/output/revision-wrong-args-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/revision-wrong-args-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/revision-wrong-args-comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-constraint-single.txt b/pkg/helm/pkg/cmd/testdata/output/search-constraint-single.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-constraint-single.txt rename to pkg/helm/pkg/cmd/testdata/output/search-constraint-single.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-constraint.txt b/pkg/helm/pkg/cmd/testdata/output/search-constraint.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-constraint.txt rename to pkg/helm/pkg/cmd/testdata/output/search-constraint.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-multiple-devel-release.txt b/pkg/helm/pkg/cmd/testdata/output/search-multiple-devel-release.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-multiple-devel-release.txt rename to pkg/helm/pkg/cmd/testdata/output/search-multiple-devel-release.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-multiple-stable-release.txt b/pkg/helm/pkg/cmd/testdata/output/search-multiple-stable-release.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-multiple-stable-release.txt rename to pkg/helm/pkg/cmd/testdata/output/search-multiple-stable-release.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-multiple-versions-constraints.txt b/pkg/helm/pkg/cmd/testdata/output/search-multiple-versions-constraints.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-multiple-versions-constraints.txt rename to pkg/helm/pkg/cmd/testdata/output/search-multiple-versions-constraints.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-multiple-versions.txt b/pkg/helm/pkg/cmd/testdata/output/search-multiple-versions.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-multiple-versions.txt rename to pkg/helm/pkg/cmd/testdata/output/search-multiple-versions.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-not-found-error.txt b/pkg/helm/pkg/cmd/testdata/output/search-not-found-error.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-not-found-error.txt rename to pkg/helm/pkg/cmd/testdata/output/search-not-found-error.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-not-found.txt b/pkg/helm/pkg/cmd/testdata/output/search-not-found.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-not-found.txt rename to pkg/helm/pkg/cmd/testdata/output/search-not-found.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-output-json.txt b/pkg/helm/pkg/cmd/testdata/output/search-output-json.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-output-json.txt rename to pkg/helm/pkg/cmd/testdata/output/search-output-json.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-output-yaml.txt b/pkg/helm/pkg/cmd/testdata/output/search-output-yaml.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-output-yaml.txt rename to pkg/helm/pkg/cmd/testdata/output/search-output-yaml.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-regex.txt b/pkg/helm/pkg/cmd/testdata/output/search-regex.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-regex.txt rename to pkg/helm/pkg/cmd/testdata/output/search-regex.txt diff --git a/pkg/helm/cmd/helm/testdata/output/search-versions-constraint.txt b/pkg/helm/pkg/cmd/testdata/output/search-versions-constraint.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/search-versions-constraint.txt rename to pkg/helm/pkg/cmd/testdata/output/search-versions-constraint.txt diff --git a/pkg/helm/cmd/helm/testdata/output/status-comp.txt b/pkg/helm/pkg/cmd/testdata/output/status-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/status-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/status-comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/status-with-desc.txt b/pkg/helm/pkg/cmd/testdata/output/status-with-desc.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/status-with-desc.txt rename to pkg/helm/pkg/cmd/testdata/output/status-with-desc.txt diff --git a/pkg/helm/cmd/helm/testdata/output/status-with-notes.txt b/pkg/helm/pkg/cmd/testdata/output/status-with-notes.txt similarity index 91% rename from pkg/helm/cmd/helm/testdata/output/status-with-notes.txt rename to pkg/helm/pkg/cmd/testdata/output/status-with-notes.txt index e992ce91..f05be6c1 100644 --- a/pkg/helm/cmd/helm/testdata/output/status-with-notes.txt +++ b/pkg/helm/pkg/cmd/testdata/output/status-with-notes.txt @@ -3,6 +3,7 @@ LAST DEPLOYED: Sat Jan 16 00:00:00 2016 NAMESPACE: default STATUS: deployed REVISION: 0 +DESCRIPTION: TEST SUITE: None NOTES: release notes diff --git a/pkg/helm/pkg/cmd/testdata/output/status-with-resources.json b/pkg/helm/pkg/cmd/testdata/output/status-with-resources.json new file mode 100644 index 00000000..af512bfd --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/status-with-resources.json @@ -0,0 +1 @@ +{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed"},"namespace":"default"} diff --git a/pkg/helm/cmd/helm/testdata/output/status-with-resources.txt b/pkg/helm/pkg/cmd/testdata/output/status-with-resources.txt similarity index 90% rename from pkg/helm/cmd/helm/testdata/output/status-with-resources.txt rename to pkg/helm/pkg/cmd/testdata/output/status-with-resources.txt index a326c3db..20763acd 100644 --- a/pkg/helm/cmd/helm/testdata/output/status-with-resources.txt +++ b/pkg/helm/pkg/cmd/testdata/output/status-with-resources.txt @@ -3,4 +3,5 @@ LAST DEPLOYED: Sat Jan 16 00:00:00 2016 NAMESPACE: default STATUS: deployed REVISION: 0 +DESCRIPTION: TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/status-with-test-suite.txt b/pkg/helm/pkg/cmd/testdata/output/status-with-test-suite.txt similarity index 96% rename from pkg/helm/cmd/helm/testdata/output/status-with-test-suite.txt rename to pkg/helm/pkg/cmd/testdata/output/status-with-test-suite.txt index 58c67e10..7c1ade45 100644 --- a/pkg/helm/cmd/helm/testdata/output/status-with-test-suite.txt +++ b/pkg/helm/pkg/cmd/testdata/output/status-with-test-suite.txt @@ -3,6 +3,7 @@ LAST DEPLOYED: Sat Jan 16 00:00:00 2016 NAMESPACE: default STATUS: deployed REVISION: 0 +DESCRIPTION: TEST SUITE: passing-test Last Started: Mon Jan 2 15:04:05 2006 Last Completed: Mon Jan 2 15:04:07 2006 diff --git a/pkg/helm/pkg/cmd/testdata/output/status-wrong-args-comp.txt b/pkg/helm/pkg/cmd/testdata/output/status-wrong-args-comp.txt new file mode 100644 index 00000000..3c537283 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/status-wrong-args-comp.txt @@ -0,0 +1,3 @@ +_activeHelp_ This command does not take any more arguments (but may accept flags). +:4 +Completion ended with directive: ShellCompDirectiveNoFileComp diff --git a/pkg/helm/pkg/cmd/testdata/output/status.json b/pkg/helm/pkg/cmd/testdata/output/status.json new file mode 100644 index 00000000..4727dd10 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/status.json @@ -0,0 +1 @@ +{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/pkg/helm/cmd/helm/testdata/output/status.txt b/pkg/helm/pkg/cmd/testdata/output/status.txt similarity index 90% rename from pkg/helm/cmd/helm/testdata/output/status.txt rename to pkg/helm/pkg/cmd/testdata/output/status.txt index a326c3db..20763acd 100644 --- a/pkg/helm/cmd/helm/testdata/output/status.txt +++ b/pkg/helm/pkg/cmd/testdata/output/status.txt @@ -3,4 +3,5 @@ LAST DEPLOYED: Sat Jan 16 00:00:00 2016 NAMESPACE: default STATUS: deployed REVISION: 0 +DESCRIPTION: TEST SUITE: None diff --git a/pkg/helm/cmd/helm/testdata/output/values.json b/pkg/helm/pkg/cmd/testdata/output/values.json similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/values.json rename to pkg/helm/pkg/cmd/testdata/output/values.json diff --git a/pkg/helm/cmd/helm/testdata/output/values.yaml b/pkg/helm/pkg/cmd/testdata/output/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/values.yaml rename to pkg/helm/pkg/cmd/testdata/output/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/output/version-comp.txt b/pkg/helm/pkg/cmd/testdata/output/version-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/version-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/version-comp.txt diff --git a/pkg/helm/cmd/helm/testdata/output/version-invalid-comp.txt b/pkg/helm/pkg/cmd/testdata/output/version-invalid-comp.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/output/version-invalid-comp.txt rename to pkg/helm/pkg/cmd/testdata/output/version-invalid-comp.txt diff --git a/pkg/helm/pkg/cmd/testdata/output/version-short.txt b/pkg/helm/pkg/cmd/testdata/output/version-short.txt new file mode 100644 index 00000000..8cf4318f --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/version-short.txt @@ -0,0 +1 @@ +v4.1 diff --git a/pkg/helm/pkg/cmd/testdata/output/version-template.txt b/pkg/helm/pkg/cmd/testdata/output/version-template.txt new file mode 100644 index 00000000..8fd8b496 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/version-template.txt @@ -0,0 +1 @@ +Version: v4.1 \ No newline at end of file diff --git a/pkg/helm/pkg/cmd/testdata/output/version.txt b/pkg/helm/pkg/cmd/testdata/output/version.txt new file mode 100644 index 00000000..1f4cf4d4 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/output/version.txt @@ -0,0 +1 @@ +version.BuildInfo{Version:"v4.1", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"} diff --git a/pkg/helm/cmd/helm/testdata/password b/pkg/helm/pkg/cmd/testdata/password similarity index 100% rename from pkg/helm/cmd/helm/testdata/password rename to pkg/helm/pkg/cmd/testdata/password diff --git a/pkg/helm/cmd/helm/testdata/repositories.yaml b/pkg/helm/pkg/cmd/testdata/repositories.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/repositories.yaml rename to pkg/helm/pkg/cmd/testdata/repositories.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/alpine/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/alpine/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/alpine/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/alpine/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/alpine/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/alpine/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/alpine/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/alpine/README.md diff --git a/pkg/helm/cmd/helm/testdata/testcharts/alpine/extra_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/alpine/extra_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/alpine/extra_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/alpine/extra_values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/alpine/more_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/alpine/more_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/alpine/more_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/alpine/more_values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/alpine/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/alpine/templates/alpine-pod.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/alpine/templates/alpine-pod.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/alpine/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/alpine/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/alpine/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/alpine/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/charts/reqsubchart/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-requirements/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-requirements/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/README.md diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/extra_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/extra_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/extra_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/extra_values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/more_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/more_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/more_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/more_values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/templates/alpine-pod.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-bad-type/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-bad-type/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/charts/reqsubchart/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/charts/reqsubchart/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/charts/reqsubchart/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/charts/reqsubchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/charts/reqsubchart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/charts/reqsubchart/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-missing-deps/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-missing-deps/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/requirements.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-bad-subcharts/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-deprecated-api/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-deprecated-api/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/templates/horizontalpodautoscaler.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-deprecated-api/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/charts/common-0.0.5.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/_helpers.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/deployment.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/ingress.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/templates/service.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/templates/service.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-lib-dep/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-lib-dep/values.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-only-crds/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml new file mode 100644 index 00000000..1d7350f1 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-only-crds/crds/test-crd.yaml @@ -0,0 +1,19 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: tests.test.io +spec: + group: test.io + names: + kind: Test + listKind: TestList + plural: tests + singular: test + scope: Namespaced + versions: + - name : v1alpha2 + served: true + storage: true + - name : v1alpha1 + served: true + storage: false diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/templates/empty.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.schema.json diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/charts/subchart-with-schema/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/templates/empty.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/values.schema.json diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-and-subchart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-and-subchart/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/templates/empty.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/templates/empty.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/values.schema.json b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/values.schema.json similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/values.schema.json rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/values.schema.json diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/values.yaml new file mode 100644 index 00000000..5a1250bf --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative-skip-validation/values.yaml @@ -0,0 +1,14 @@ +firstname: John +lastname: Doe +age: -5 +likesCoffee: true +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/templates/empty.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/templates/empty.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/templates/empty.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/values.schema.json b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/values.schema.json new file mode 100644 index 00000000..4df89bbe --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/values.yaml new file mode 100644 index 00000000..5a1250bf --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema-negative/values.yaml @@ -0,0 +1,14 @@ +firstname: John +lastname: Doe +age: -5 +likesCoffee: true +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/Chart.yaml new file mode 100644 index 00000000..395d24f6 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +description: Empty testing chart +home: https://k8s.io/helm +name: empty +sources: +- https://github.com/kubernetes/helm +version: 0.1.0 diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/extra-values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/extra-values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-schema/extra-values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/extra-values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/empty/templates/empty.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/templates/empty.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/empty/templates/empty.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/templates/empty.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/values.schema.json b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/values.schema.json new file mode 100644 index 00000000..4df89bbe --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/values.schema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "addresses": { + "description": "List of addresses", + "items": { + "properties": { + "city": { + "type": "string" + }, + "number": { + "type": "number" + }, + "street": { + "type": "string" + } + }, + "type": "object" + }, + "type": "array" + }, + "age": { + "description": "Age", + "minimum": 0, + "type": "integer" + }, + "employmentInfo": { + "properties": { + "salary": { + "minimum": 0, + "type": "number" + }, + "title": { + "type": "string" + } + }, + "required": [ + "salary" + ], + "type": "object" + }, + "firstname": { + "description": "First name", + "type": "string" + }, + "lastname": { + "type": "string" + }, + "likesCoffee": { + "type": "boolean" + }, + "phoneNumbers": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "firstname", + "lastname", + "addresses", + "employmentInfo" + ], + "title": "Values", + "type": "object" +} diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/values.yaml new file mode 100644 index 00000000..042dea66 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-schema/values.yaml @@ -0,0 +1,17 @@ +firstname: John +lastname: Doe +age: 25 +likesCoffee: true +employmentInfo: + title: Software Developer + salary: 100000 +addresses: + - city: Springfield + street: Main + number: 12345 + - city: New York + street: Broadway + number: 67890 +phoneNumbers: + - "(888) 888-8888" + - "(555) 555-5555" diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/Chart.yaml new file mode 100644 index 00000000..46d069e1 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +description: Chart with Kubernetes Secret +name: chart-with-secret +version: 0.0.1 diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/templates/configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/templates/configmap.yaml new file mode 100644 index 00000000..ce9c27d5 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/templates/configmap.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: test-configmap +data: + foo: bar diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/templates/secret.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/templates/secret.yaml new file mode 100644 index 00000000..b1e1cff5 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-secret/templates/secret.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test-secret +stringData: + foo: bar diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/charts/subchart-with-notes/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-notes/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.lock b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/Chart.lock similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.lock rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/Chart.lock diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/charts/subchart-with-notes/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-subchart-update/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/charts/common-0.0.5.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/_helpers.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/deployment.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/ingress.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/templates/service.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-archive-dep/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md new file mode 100755 index 00000000..cafadcd7 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/README.md @@ -0,0 +1,831 @@ +# Common: The Helm Helper Chart + +This chart is designed to make it easier for you to build and maintain Helm +charts. + +It provides utilities that reflect best practices of Kubernetes chart development, +making it faster for you to write charts. + +## Tips + +A few tips for working with Common: + +- Be careful when using functions that generate random data (like `common.fullname.unique`). + They may trigger unwanted upgrades or have other side effects. + +In this document, we use `release-name` as the name of the release. + +## Resource Kinds + +Kubernetes defines a variety of resource kinds, from `Secret` to `StatefulSet`. +We define some of the most common kinds in a way that lets you easily work with +them. + +The resource kind templates are designed to make it much faster for you to +define _basic_ versions of these resources. They allow you to extend and modify +just what you need, without having to copy around lots of boilerplate. + +To make use of these templates you must define a template that will extend the +base template (though it can be empty). The name of this template is then passed +to the base template, for example: + +```yaml +{{- template "common.service" (list . "mychart.service") -}} +{{- define "mychart.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} +``` + +Note that the `common.service` template defines two parameters: + + - The root context (usually `.`) + - A template name containing the service definition overrides + +A limitation of the Go template library is that a template can only take a +single argument. The `list` function is used to work around this by constructing +a list or array of arguments that is passed to the template. + +The `common.service` template is responsible for rendering the templates with +the root context and merging any overrides. As you can see, this makes it very +easy to create a basic `Service` resource without having to copy around the +standard metadata and labels. + +Each implemented base resource is described in greater detail below. + +### `common.service` + +The `common.service` template creates a basic `Service` resource with the +following defaults: + +- Service type (ClusterIP, NodePort, LoadBalancer) made configurable by `.Values.service.type` +- Named port `http` configured on port 80 +- Selector set to `app: {{ template "common.name" }}, release: {{ .Release.Name | quote }}` to match the default used in the `Deployment` resource + +Example template: + +```yaml +{{- template "common.service" (list . "mychart.mail.service") -}} +{{- define "mychart.mail.service" -}} +metadata: + name: {{ template "common.fullname" . }}-mail # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: mail +spec: + ports: # composes the `ports` section of the service definition. + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: # this is appended to the default selector + protocol: mail +{{- end -}} +--- +{{ template "common.service" (list . "mychart.web.service") -}} +{{- define "mychart.web.service" -}} +metadata: + name: {{ template "common.fullname" . }}-www # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: www +spec: + ports: # composes the `ports` section of the service definition. + - name: www + port: 80 + targetPort: 8080 +{{- end -}} +``` + +The above template defines _two_ services: a web service and a mail service. + +The most important part of a service definition is the `ports` object, which +defines the ports that this service will listen on. Most of the time, +`selector` is computed for you. But you can replace it or add to it. + +The output of the example above is: + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app: service + chart: service-0.1.0 + heritage: Tiller + protocol: mail + release: release-name + name: release-name-service-mail +spec: + ports: + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: + app: service + release: release-name + protocol: mail + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app: service + chart: service-0.1.0 + heritage: Tiller + protocol: www + release: release-name + name: release-name-service-www +spec: + ports: + - name: www + port: 80 + targetPort: 8080 + type: ClusterIP +``` + +## `common.deployment` + +The `common.deployment` template defines a basic `Deployment`. Underneath the +hood, it uses `common.container` (see next section). + +By default, the pod template within the deployment defines the labels `app: {{ template "common.name" . }}` +and `release: {{ .Release.Name | quote }` as this is also used as the selector. The +standard set of labels are not used as some of these can change during upgrades, +which causes the replica sets and pods to not correctly match. + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + replicas: {{ .Values.replicaCount }} +{{- end -}} +``` + +## `common.container` + +The `common.container` template creates a basic `Container` spec to be used +within a `Deployment` or `ReplicaSet`. It holds the following defaults: + +- The name is set to the chart name +- Uses `.Values.image` to describe the image to run, with the following spec: + ```yaml + image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + ``` +- Exposes the named port `http` as port 80 +- Lays out the compute resources using `.Values.resources` + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +## Define overrides for your Container here, e.g. +livenessProbe: + httpGet: + path: / + port: 80 +readinessProbe: + httpGet: + path: / + port: 80 +{{- end -}} +``` + +The above example creates a `Deployment` resource which makes use of the +`common.container` template to populate the PodSpec's container list. The usage +of this template is similar to the other resources, you must define and +reference a template that contains overrides for the container object. + +The most important part of a container definition is the image you want to run. +As mentioned above, this is derived from `.Values.image` by default. It is a +best practice to define the image, tag and pull policy in your charts' values as +this makes it easy for an operator to change the image registry, or use a +specific tag or version. Another example of configuration that should be exposed +to chart operators is the container's required compute resources, as this is +also very specific to an operators environment. An example `values.yaml` for +your chart could look like: + +```yaml +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +The output of running the above values through the earlier template is: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app: deployment + chart: deployment-0.1.0 + heritage: Tiller + release: release-name + name: release-name-deployment +spec: + template: + metadata: + labels: + app: deployment + spec: + containers: + - image: nginx:stable + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: 80 + name: deployment + ports: + - containerPort: 80 + name: http + readinessProbe: + httpGet: + path: / + port: 80 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +## `common.configmap` + +The `common.configmap` template creates an empty `ConfigMap` resource that you +can override with your configuration. + +Example use: + +```yaml +{{- template "common.configmap" (list . "mychart.configmap") -}} +{{- define "mychart.configmap" -}} +data: + zeus: cat + athena: cat + julius: cat + one: |- + {{ .Files.Get "file1.txt" }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: cat + julius: cat + one: This is a file. + zeus: cat +kind: ConfigMap +metadata: + labels: + app: configmap + chart: configmap-0.1.0 + heritage: Tiller + release: release-name + name: release-name-configmap +``` + +## `common.secret` + +The `common.secret` template creates an empty `Secret` resource that you +can override with your secrets. + +Example use: + +```yaml +{{- template "common.secret" (list . "mychart.secret") -}} +{{- define "mychart.secret" -}} +data: + zeus: {{ print "cat" | b64enc }} + athena: {{ print "cat" | b64enc }} + julius: {{ print "cat" | b64enc }} + one: |- + {{ .Files.Get "file1.txt" | b64enc }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: Y2F0 + julius: Y2F0 + one: VGhpcyBpcyBhIGZpbGUuCg== + zeus: Y2F0 +kind: Secret +metadata: + labels: + app: secret + chart: secret-0.1.0 + heritage: Tiller + release: release-name + name: release-name-secret +type: Opaque +``` + +## `common.ingress` + +The `common.ingress` template is designed to give you a well-defined `Ingress` +resource, that can be configured using `.Values.ingress`. An example values file +that can be used to configure the `Ingress` resource is: + +```yaml +ingress: + hosts: + - chart-example.local + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + tls: + - secretName: chart-example-tls + hosts: + - chart-example.local +``` + +Example use: + +```yaml +{{- template "common.ingress" (list . "mychart.ingress") -}} +{{- define "mychart.ingress" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + labels: + app: ingress + chart: ingress-0.1.0 + heritage: Tiller + release: release-name + name: release-name-ingress +spec: + rules: + - host: chart-example.local + http: + paths: + - backend: + serviceName: release-name-ingress + servicePort: 80 + path: / + tls: + - hosts: + - chart-example.local + secretName: chart-example-tls +``` + +## `common.persistentvolumeclaim` + +`common.persistentvolumeclaim` can be used to easily add a +`PersistentVolumeClaim` resource to your chart that can be configured using +`.Values.persistence`: + +| Value | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------- | +| persistence.enabled | Whether or not to claim a persistent volume. If false, `common.volume.pvc` will use an emptyDir instead | +| persistence.storageClass | `StorageClass` name | +| persistence.accessMode | Access mode for persistent volume | +| persistence.size | Size of persistent volume | +| persistence.existingClaim | If defined, `PersistentVolumeClaim` is not created and `common.volume.pvc` helper uses this claim | + +An example values file that can be used to configure the +`PersistentVolumeClaim` resource is: + +```yaml +persistence: + enabled: true + storageClass: fast + accessMode: ReadWriteOnce + size: 8Gi +``` + +Example use: + +```yaml +{{- template "common.persistentvolumeclaim" (list . "mychart.persistentvolumeclaim") -}} +{{- define "mychart.persistentvolumeclaim" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app: persistentvolumeclaim + chart: persistentvolumeclaim-0.1.0 + heritage: Tiller + release: release-name + name: release-name-persistentvolumeclaim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: "fast" +``` + +## Partial API Objects + +When writing Kubernetes resources, you may find the following helpers useful to +construct parts of the spec. + +### EnvVar + +Use the EnvVar helpers within a container spec to simplify specifying key-value +environment variables or referencing secrets as values. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +{{- $fullname := include "common.fullname" . -}} +env: +- {{ template "common.envvar.value" (list "ZEUS" "cat") }} +- {{ template "common.envvar.secret" (list "ATHENA" "secret-name" "athena") }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + containers: + - env: + - name: ZEUS + value: cat + - name: ATHENA + valueFrom: + secretKeyRef: + key: athena + name: secret-name +... +``` + +### Volume + +Use the Volume helpers within a `Deployment` spec to help define ConfigMap and +PersistentVolumeClaim volumes. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + volumes: + - {{ template "common.volume.configMap" (list "config" "configmap-name") }} + - {{ template "common.volume.pvc" (list "data" "pvc-name" .Values.persistence) }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + volumes: + - configMap: + name: configmap-name + name: config + - name: data + persistentVolumeClaim: + claimName: pvc-name +... +``` + +The `common.volume.pvc` helper uses the following configuration from the `.Values.persistence` object: + +| Value | Description | +| ------------------------- | ----------------------------------------------------- | +| persistence.enabled | If false, creates an `emptyDir` instead | +| persistence.existingClaim | If set, uses this instead of the passed in claim name | + +## Utilities + +### `common.fullname` + +The `common.fullname` template generates a name suitable for the `name:` field +in Kubernetes metadata. It is used like this: + +```yaml +name: {{ template "common.fullname" . }} +``` + +The following different values can influence it: + +```yaml +# By default, fullname uses '{{ .Release.Name }}-{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +fullnameOverride: "some-name" + +# This adds a prefix +fullnamePrefix: "pre-" +# This appends a suffix +fullnameSuffix: "-suf" + +# Global versions of the above +global: + fullnamePrefix: "pp-" + fullnameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for release "happy-panda" and chart "wordpress" +name: happy-panda-wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.fullname" -}} + {{ template "common.fullname" . }}-my-stuff +{{- end -}} +``` + +### `common.fullname.unique` + +The `common.fullname.unique` variant of fullname appends a unique seven-character +sequence to the end of the common name field. + +This takes all of the same parameters as `common.fullname` + +Example template: + +```yaml +uniqueName: {{ template "common.fullname.unique" . }} +``` + +Example output: + +```yaml +uniqueName: release-name-fullname-jl0dbwx +``` + +It is also impacted by the prefix and suffix definitions, as well as by +`.Values.fullnameOverride` + +Note that the effective maximum length of this function is 63 characters, not 54. + +### `common.name` + +The `common.name` template generates a name suitable for the `app` label. It is used like this: + +```yaml +app: {{ template "common.name" . }} +``` + +The following different values can influence it: + +```yaml +# By default, name uses '{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +nameOverride: "some-name" + +# This adds a prefix +namePrefix: "pre-" +# This appends a suffix +nameSuffix: "-suf" + +# Global versions of the above +global: + namePrefix: "pp-" + nameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for chart "wordpress" +name: wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.name" -}} + {{ template "common.name" . }}-my-stuff +{{- end -}} +``` + +### `common.metadata` + +The `common.metadata` helper generates the `metadata:` section of a Kubernetes +resource. + +This takes three objects: + - .top: top context + - .fullnameOverride: override the fullname with this name + - .metadata + - .labels: key/value list of labels + - .annotations: key/value list of annotations + - .hook: name(s) of hook(s) + +It generates standard labels, annotations, hooks, and a name field. + +Example template: + +```yaml +{{ template "common.metadata" (dict "top" . "metadata" .Values.bio) }} +--- +{{ template "common.metadata" (dict "top" . "metadata" .Values.pet "fullnameOverride" .Values.pet.fullnameOverride) }} +``` + +Example values: + +```yaml +bio: + name: example + labels: + first: matt + last: butcher + nick: technosophos + annotations: + format: bio + destination: archive + hook: pre-install + +pet: + fullnameOverride: Zeus + +``` + +Example output: + +```yaml +metadata: + name: release-name-metadata + labels: + app: metadata + heritage: "Tiller" + release: "release-name" + chart: metadata-0.1.0 + first: "matt" + last: "butcher" + nick: "technosophos" + annotations: + "destination": "archive" + "format": "bio" + "helm.sh/hook": "pre-install" +--- +metadata: + name: Zeus + labels: + app: metadata + heritage: "Tiller" + release: "release-name" + chart: metadata-0.1.0 + annotations: +``` + +Most of the common templates that define a resource type (e.g. `common.configmap` +or `common.job`) use this to generate the metadata, which means they inherit +the same `labels`, `annotations`, `nameOverride`, and `hook` fields. + +### `common.labelize` + +`common.labelize` turns a map into a set of labels. + +Example template: + +```yaml +{{- $map := dict "first" "1" "second" "2" "third" "3" -}} +{{- template "common.labelize" $map -}} +``` + +Example output: + +```yaml +first: "1" +second: "2" +third: "3" +``` + +### `common.labels.standard` + +`common.labels.standard` prints the standard set of labels. + +Example usage: + +``` +{{ template "common.labels.standard" . }} +``` + +Example output: + +```yaml +app: labelizer +heritage: "Tiller" +release: "release-name" +chart: labelizer-0.1.0 +``` + +### `common.hook` + +The `common.hook` template is a convenience for defining hooks. + +Example template: + +```yaml +{{ template "common.hook" "pre-install,post-install" }} +``` + +Example output: + +```yaml +"helm.sh/hook": "pre-install,post-install" +``` + +### `common.chartref` + +The `common.chartref` helper prints the chart name and version, escaped to be +legal in a Kubernetes label field. + +Example template: + +```yaml +chartref: {{ template "common.chartref" . }} +``` + +For the chart `foo` with version `1.2.3-beta.55+1234`, this will render: + +```yaml +chartref: foo-1.2.3-beta.55_1234 +``` + +(Note that `+` is an illegal character in label values) diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_chartref.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_configmap.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_container.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_deployment.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_envvar.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_fullname.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_ingress.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_annotations.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_metadata_labels.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_name.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_persistentvolumeclaim.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_secret.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_service.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_util.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/_volume.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/templates/configmap.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/charts/common/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/NOTES.txt diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/_helpers.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/deployment.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/ingress.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/templates/service.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-lib-dep/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-lib-dep/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tar.gz b/pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tar.gz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tar.gz rename to pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tar.gz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.1.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.2.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.2.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.2.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.3.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/compressedchart-0.3.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-0.3.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/compressedchart-with-hyphens-0.1.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/deprecated/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/deprecated/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/deprecated/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/deprecated/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/deprecated/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/deprecated/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/deprecated/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/deprecated/README.md diff --git a/pkg/helm/cmd/helm/testdata/testcharts/empty/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/empty/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/empty/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/empty/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/empty/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/empty/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/empty/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/empty/README.md diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/empty/templates/empty.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/empty/templates/empty.yaml new file mode 100644 index 00000000..c80812f6 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/empty/templates/empty.yaml @@ -0,0 +1 @@ +# This file is intentionally blank diff --git a/pkg/helm/cmd/helm/testdata/testcharts/empty/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/empty/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/empty/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/empty/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-7233/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-7233/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-7233/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-7233/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/charts/alpine-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/charts/alpine-0.1.0.tgz new file mode 100644 index 00000000..21e50abe Binary files /dev/null and b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/charts/alpine-0.1.0.tgz differ diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-7233/requirements.lock b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/requirements.lock similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-7233/requirements.lock rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/requirements.lock diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-7233/requirements.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/requirements.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-7233/requirements.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/requirements.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-7233/templates/configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/templates/configmap.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-7233/templates/configmap.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/templates/configmap.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-7233/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-7233/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-7233/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-9027/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-9027/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/charts/subchart/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/charts/subchart/templates/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/charts/subchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-9027/charts/subchart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/charts/subchart/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-9027/templates/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/templates/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-9027/templates/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/templates/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue-9027/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue-9027/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue-9027/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue1979/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue1979/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue1979/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue1979/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue1979/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/issue1979/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue1979/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/issue1979/README.md diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue1979/extra_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue1979/extra_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue1979/extra_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue1979/extra_values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue1979/more_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue1979/more_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue1979/more_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue1979/more_values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue1979/templates/alpine-pod.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue1979/templates/alpine-pod.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue1979/templates/alpine-pod.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue1979/templates/alpine-pod.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/issue1979/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/issue1979/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/issue1979/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/issue1979/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/README.md new file mode 100644 index 00000000..f69ff1c0 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/README.md @@ -0,0 +1,831 @@ +# Common: The Helm Helper Chart + +This chart is designed to make it easier for you to build and maintain Helm +charts. + +It provides utilities that reflect best practices of Kubernetes chart development, +making it faster for you to write charts. + +## Tips + +A few tips for working with Common: + +- Be careful when using functions that generate random data (like `common.fullname.unique`). + They may trigger unwanted upgrades or have other side effects. + +In this document, we use `release-name` as the name of the release. + +## Resource Kinds + +Kubernetes defines a variety of resource kinds, from `Secret` to `StatefulSet`. +We define some of the most common kinds in a way that lets you easily work with +them. + +The resource kind templates are designed to make it much faster for you to +define _basic_ versions of these resources. They allow you to extend and modify +just what you need, without having to copy around lots of boilerplate. + +To make use of these templates you must define a template that will extend the +base template (though it can be empty). The name of this template is then passed +to the base template, for example: + +```yaml +{{- template "common.service" (list . "mychart.service") -}} +{{- define "mychart.service" -}} +## Define overrides for your Service resource here, e.g. +# metadata: +# labels: +# custom: label +# spec: +# ports: +# - port: 8080 +{{- end -}} +``` + +Note that the `common.service` template defines two parameters: + + - The root context (usually `.`) + - A template name containing the service definition overrides + +A limitation of the Go template library is that a template can only take a +single argument. The `list` function is used to work around this by constructing +a list or array of arguments that is passed to the template. + +The `common.service` template is responsible for rendering the templates with +the root context and merging any overrides. As you can see, this makes it very +easy to create a basic `Service` resource without having to copy around the +standard metadata and labels. + +Each implemented base resource is described in greater detail below. + +### `common.service` + +The `common.service` template creates a basic `Service` resource with the +following defaults: + +- Service type (ClusterIP, NodePort, LoadBalancer) made configurable by `.Values.service.type` +- Named port `http` configured on port 80 +- Selector set to `app.kubernetes.io/name: {{ template "common.name" }}, app.kubernetes.io/instance: {{ .Release.Name | quote }}` to match the default used in the `Deployment` resource + +Example template: + +```yaml +{{- template "common.service" (list . "mychart.mail.service") -}} +{{- define "mychart.mail.service" -}} +metadata: + name: {{ template "common.fullname" . }}-mail # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: mail +spec: + ports: # composes the `ports` section of the service definition. + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: # this is appended to the default selector + protocol: mail +{{- end -}} +--- +{{ template "common.service" (list . "mychart.web.service") -}} +{{- define "mychart.web.service" -}} +metadata: + name: {{ template "common.fullname" . }}-www # overrides the default name to add a suffix + labels: # appended to the labels section + protocol: www +spec: + ports: # composes the `ports` section of the service definition. + - name: www + port: 80 + targetPort: 8080 +{{- end -}} +``` + +The above template defines _two_ services: a web service and a mail service. + +The most important part of a service definition is the `ports` object, which +defines the ports that this service will listen on. Most of the time, +`selector` is computed for you. But you can replace it or add to it. + +The output of the example above is: + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + helm.sh/chart: service-0.1.0 + app.kubernetes.io/managed-by: Helm + protocol: mail + app.kubernetes.io/instance: release-name + name: release-name-service-mail +spec: + ports: + - name: smtp + port: 25 + targetPort: 25 + - name: imaps + port: 993 + targetPort: 993 + selector: + app.kubernetes.io/name: service + app.kubernetes.io/instance: release-name + protocol: mail + type: ClusterIP +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: service + helm.sh/chart: service-0.1.0 + app.kubernetes.io/managed-by: Helm + protocol: www + app.kubernetes.io/instance: release-name + name: release-name-service-www +spec: + ports: + - name: www + port: 80 + targetPort: 8080 + type: ClusterIP +``` + +## `common.deployment` + +The `common.deployment` template defines a basic `Deployment`. Underneath the +hood, it uses `common.container` (see next section). + +By default, the pod template within the deployment defines the labels `app: {{ template "common.name" . }}` +and `release: {{ .Release.Name | quote }` as this is also used as the selector. The +standard set of labels are not used as some of these can change during upgrades, +which causes the replica sets and pods to not correctly match. + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + replicas: {{ .Values.replicaCount }} +{{- end -}} +``` + +## `common.container` + +The `common.container` template creates a basic `Container` spec to be used +within a `Deployment` or `ReplicaSet`. It holds the following defaults: + +- The name is set to the chart name +- Uses `.Values.image` to describe the image to run, with the following spec: + ```yaml + image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent + ``` +- Exposes the named port `http` as port 80 +- Lays out the compute resources using `.Values.resources` + +Example use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +## Define overrides for your Deployment resource here, e.g. +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +## Define overrides for your Container here, e.g. +livenessProbe: + httpGet: + path: / + port: 80 +readinessProbe: + httpGet: + path: / + port: 80 +{{- end -}} +``` + +The above example creates a `Deployment` resource which makes use of the +`common.container` template to populate the PodSpec's container list. The usage +of this template is similar to the other resources, you must define and +reference a template that contains overrides for the container object. + +The most important part of a container definition is the image you want to run. +As mentioned above, this is derived from `.Values.image` by default. It is a +best practice to define the image, tag and pull policy in your charts' values as +this makes it easy for an operator to change the image registry, or use a +specific tag or version. Another example of configuration that should be exposed +to chart operators is the container's required compute resources, as this is +also very specific to an operators environment. An example `values.yaml` for +your chart could look like: + +```yaml +image: + repository: nginx + tag: stable + pullPolicy: IfNotPresent +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +The output of running the above values through the earlier template is: + +```yaml +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/name: deployment + helm.sh/chart: deployment-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-deployment +spec: + template: + metadata: + labels: + app.kubernetes.io/name: deployment + spec: + containers: + - image: nginx:stable + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: 80 + name: deployment + ports: + - containerPort: 80 + name: http + readinessProbe: + httpGet: + path: / + port: 80 + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 128Mi +``` + +## `common.configmap` + +The `common.configmap` template creates an empty `ConfigMap` resource that you +can override with your configuration. + +Example use: + +```yaml +{{- template "common.configmap" (list . "mychart.configmap") -}} +{{- define "mychart.configmap" -}} +data: + zeus: cat + athena: cat + julius: cat + one: |- + {{ .Files.Get "file1.txt" }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: cat + julius: cat + one: This is a file. + zeus: cat +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/name: configmap + helm.sh/chart: configmap-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-configmap +``` + +## `common.secret` + +The `common.secret` template creates an empty `Secret` resource that you +can override with your secrets. + +Example use: + +```yaml +{{- template "common.secret" (list . "mychart.secret") -}} +{{- define "mychart.secret" -}} +data: + zeus: {{ print "cat" | b64enc }} + athena: {{ print "cat" | b64enc }} + julius: {{ print "cat" | b64enc }} + one: |- + {{ .Files.Get "file1.txt" | b64enc }} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +data: + athena: Y2F0 + julius: Y2F0 + one: VGhpcyBpcyBhIGZpbGUuCg== + zeus: Y2F0 +kind: Secret +metadata: + labels: + app.kubernetes.io/name: secret + helm.sh/chart: secret-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-secret +type: Opaque +``` + +## `common.ingress` + +The `common.ingress` template is designed to give you a well-defined `Ingress` +resource, that can be configured using `.Values.ingress`. An example values file +that can be used to configure the `Ingress` resource is: + +```yaml +ingress: + hosts: + - chart-example.local + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + tls: + - secretName: chart-example-tls + hosts: + - chart-example.local +``` + +Example use: + +```yaml +{{- template "common.ingress" (list . "mychart.ingress") -}} +{{- define "mychart.ingress" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: nginx + kubernetes.io/tls-acme: "true" + labels: + app.kubernetes.io/name: ingress + helm.sh/chart: ingress-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-ingress +spec: + rules: + - host: chart-example.local + http: + paths: + - backend: + serviceName: release-name-ingress + servicePort: 80 + path: / + tls: + - hosts: + - chart-example.local + secretName: chart-example-tls +``` + +## `common.persistentvolumeclaim` + +`common.persistentvolumeclaim` can be used to easily add a +`PersistentVolumeClaim` resource to your chart that can be configured using +`.Values.persistence`: + +| Value | Description | +| ------------------------- | ------------------------------------------------------------------------------------------------------- | +| persistence.enabled | Whether or not to claim a persistent volume. If false, `common.volume.pvc` will use an emptyDir instead | +| persistence.storageClass | `StorageClass` name | +| persistence.accessMode | Access mode for persistent volume | +| persistence.size | Size of persistent volume | +| persistence.existingClaim | If defined, `PersistentVolumeClaim` is not created and `common.volume.pvc` helper uses this claim | + +An example values file that can be used to configure the +`PersistentVolumeClaim` resource is: + +```yaml +persistence: + enabled: true + storageClass: fast + accessMode: ReadWriteOnce + size: 8Gi +``` + +Example use: + +```yaml +{{- template "common.persistentvolumeclaim" (list . "mychart.persistentvolumeclaim") -}} +{{- define "mychart.persistentvolumeclaim" -}} +{{- end -}} +``` + +Output: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + labels: + app.kubernetes.io/name: persistentvolumeclaim + helm.sh/chart: persistentvolumeclaim-0.1.0 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/instance: release-name + name: release-name-persistentvolumeclaim +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 8Gi + storageClassName: "fast" +``` + +## Partial API Objects + +When writing Kubernetes resources, you may find the following helpers useful to +construct parts of the spec. + +### EnvVar + +Use the EnvVar helpers within a container spec to simplify specifying key-value +environment variables or referencing secrets as values. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + containers: + - {{ template "common.container" (list . "mychart.deployment.container") }} +{{- end -}} +{{- define "mychart.deployment.container" -}} +{{- $fullname := include "common.fullname" . -}} +env: +- {{ template "common.envvar.value" (list "ZEUS" "cat") }} +- {{ template "common.envvar.secret" (list "ATHENA" "secret-name" "athena") }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + containers: + - env: + - name: ZEUS + value: cat + - name: ATHENA + valueFrom: + secretKeyRef: + key: athena + name: secret-name +... +``` + +### Volume + +Use the Volume helpers within a `Deployment` spec to help define ConfigMap and +PersistentVolumeClaim volumes. + +Example Use: + +```yaml +{{- template "common.deployment" (list . "mychart.deployment") -}} +{{- define "mychart.deployment" -}} +spec: + template: + spec: + volumes: + - {{ template "common.volume.configMap" (list "config" "configmap-name") }} + - {{ template "common.volume.pvc" (list "data" "pvc-name" .Values.persistence) }} +{{- end -}} +``` + +Output: + +```yaml +... + spec: + volumes: + - configMap: + name: configmap-name + name: config + - name: data + persistentVolumeClaim: + claimName: pvc-name +... +``` + +The `common.volume.pvc` helper uses the following configuration from the `.Values.persistence` object: + +| Value | Description | +| ------------------------- | ----------------------------------------------------- | +| persistence.enabled | If false, creates an `emptyDir` instead | +| persistence.existingClaim | If set, uses this instead of the passed in claim name | + +## Utilities + +### `common.fullname` + +The `common.fullname` template generates a name suitable for the `name:` field +in Kubernetes metadata. It is used like this: + +```yaml +name: {{ template "common.fullname" . }} +``` + +The following different values can influence it: + +```yaml +# By default, fullname uses '{{ .Release.Name }}-{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +fullnameOverride: "some-name" + +# This adds a prefix +fullnamePrefix: "pre-" +# This appends a suffix +fullnameSuffix: "-suf" + +# Global versions of the above +global: + fullnamePrefix: "pp-" + fullnameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for release "happy-panda" and chart "wordpress" +name: happy-panda-wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.fullname" -}} + {{ template "common.fullname" . }}-my-stuff +{{- end -}} +``` + +### `common.fullname.unique` + +The `common.fullname.unique` variant of fullname appends a unique seven-character +sequence to the end of the common name field. + +This takes all of the same parameters as `common.fullname` + +Example template: + +```yaml +uniqueName: {{ template "common.fullname.unique" . }} +``` + +Example output: + +```yaml +uniqueName: release-name-fullname-jl0dbwx +``` + +It is also impacted by the prefix and suffix definitions, as well as by +`.Values.fullnameOverride` + +Note that the effective maximum length of this function is 63 characters, not 54. + +### `common.name` + +The `common.name` template generates a name suitable for the `app` label. It is used like this: + +```yaml +app: {{ template "common.name" . }} +``` + +The following different values can influence it: + +```yaml +# By default, name uses '{{ .Chart.Name }}'. This +# overrides that and uses the given string instead. +nameOverride: "some-name" + +# This adds a prefix +namePrefix: "pre-" +# This appends a suffix +nameSuffix: "-suf" + +# Global versions of the above +global: + namePrefix: "pp-" + nameSuffix: "-ps" +``` + +Example output: + +```yaml +--- +# with the values above +name: pp-pre-some-name-suf-ps + +--- +# the default, for chart "wordpress" +name: wordpress +``` + +Output of this function is truncated at 54 characters, which leaves 9 additional +characters for customized overriding. Thus you can easily extend this name +in your own charts: + +```yaml +{{- define "my.name" -}} + {{ template "common.name" . }}-my-stuff +{{- end -}} +``` + +### `common.metadata` + +The `common.metadata` helper generates the `metadata:` section of a Kubernetes +resource. + +This takes three objects: + - .top: top context + - .fullnameOverride: override the fullname with this name + - .metadata + - .labels: key/value list of labels + - .annotations: key/value list of annotations + - .hook: name(s) of hook(s) + +It generates standard labels, annotations, hooks, and a name field. + +Example template: + +```yaml +{{ template "common.metadata" (dict "top" . "metadata" .Values.bio) }} +--- +{{ template "common.metadata" (dict "top" . "metadata" .Values.pet "fullnameOverride" .Values.pet.fullnameOverride) }} +``` + +Example values: + +```yaml +bio: + name: example + labels: + first: matt + last: butcher + nick: technosophos + annotations: + format: bio + destination: archive + hook: pre-install + +pet: + fullnameOverride: Zeus + +``` + +Example output: + +```yaml +metadata: + name: release-name-metadata + labels: + app.kubernetes.io/name: metadata + app.kubernetes.io/managed-by: "Helm" + app.kubernetes.io/instance: "release-name" + helm.sh/chart: metadata-0.1.0 + first: "matt" + last: "butcher" + nick: "technosophos" + annotations: + "destination": "archive" + "format": "bio" + "helm.sh/hook": "pre-install" +--- +metadata: + name: Zeus + labels: + app.kubernetes.io/name: metadata + app.kubernetes.io/managed-by: "Helm" + app.kubernetes.io/instance: "release-name" + helm.sh/chart: metadata-0.1.0 + annotations: +``` + +Most of the common templates that define a resource type (e.g. `common.configmap` +or `common.job`) use this to generate the metadata, which means they inherit +the same `labels`, `annotations`, `nameOverride`, and `hook` fields. + +### `common.labelize` + +`common.labelize` turns a map into a set of labels. + +Example template: + +```yaml +{{- $map := dict "first" "1" "second" "2" "third" "3" -}} +{{- template "common.labelize" $map -}} +``` + +Example output: + +```yaml +first: "1" +second: "2" +third: "3" +``` + +### `common.labels.standard` + +`common.labels.standard` prints the standard set of labels. + +Example usage: + +``` +{{ template "common.labels.standard" . }} +``` + +Example output: + +```yaml +app.kubernetes.io/name: labelizer +app.kubernetes.io/managed-by: "Tiller" +app.kubernetes.io/instance: "release-name" +helm.sh/chart: labelizer-0.1.0 +``` + +### `common.hook` + +The `common.hook` template is a convenience for defining hooks. + +Example template: + +```yaml +{{ template "common.hook" "pre-install,post-install" }} +``` + +Example output: + +```yaml +"helm.sh/hook": "pre-install,post-install" +``` + +### `common.chartref` + +The `common.chartref` helper prints the chart name and version, escaped to be +legal in a Kubernetes label field. + +Example template: + +```yaml +chartref: {{ template "common.chartref" . }} +``` + +For the chart `foo` with version `1.2.3-beta.55+1234`, this will render: + +```yaml +chartref: foo-1.2.3-beta.55_1234 +``` + +(Note that `+` is an illegal character in label values) diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_chartref.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_chartref.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_chartref.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_chartref.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_configmap.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_configmap.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_configmap.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_container.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_container.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_container.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_container.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_deployment.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_deployment.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_deployment.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_deployment.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_envvar.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_envvar.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_envvar.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_envvar.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_fullname.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_fullname.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_fullname.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_fullname.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_ingress.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_ingress.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_ingress.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_ingress.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_metadata.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_metadata.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_metadata_annotations.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_metadata_labels.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_name.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_name.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_name.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_name.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_persistentvolumeclaim.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_secret.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_secret.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_secret.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_secret.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_service.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_service.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_service.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_util.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_util.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_util.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_util.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_volume.tpl b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_volume.tpl similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/templates/_volume.tpl rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/templates/_volume.tpl diff --git a/pkg/helm/cmd/helm/testdata/testcharts/lib-chart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/lib-chart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/lib-chart/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/object-order/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/object-order/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/object-order/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/object-order/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml b/pkg/helm/pkg/cmd/testdata/testcharts/object-order/templates/01-a.yml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/object-order/templates/01-a.yml rename to pkg/helm/pkg/cmd/testdata/testcharts/object-order/templates/01-a.yml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml b/pkg/helm/pkg/cmd/testdata/testcharts/object-order/templates/02-b.yml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/object-order/templates/02-b.yml rename to pkg/helm/pkg/cmd/testdata/testcharts/object-order/templates/02-b.yml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/object-order/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/object-order/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/oci-dependent-chart-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/oci-dependent-chart-0.1.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/oci-dependent-chart-0.1.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/oci-dependent-chart-0.1.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/pre-release-chart-0.1.0-alpha.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest-0.1.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest-0.1.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest-0.1.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/Chart.lock b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/Chart.lock similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/Chart.lock rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/Chart.lock diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart2/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart2/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart2/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/charts/reqsubchart3-0.2.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/reqtest/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/reqtest/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/reqtest/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/reqtest/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov b/pkg/helm/pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz.prov similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest-0.1.0.tgz.prov rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz.prov diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/.helmignore b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/.helmignore similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/.helmignore rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest/.helmignore diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/Chart.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/README.md b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/README.md similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/README.md rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/README.md diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml new file mode 100644 index 00000000..5bbae10a --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/templates/alpine-pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{.Release.Name}}-{{.Chart.Name}} + labels: + app.kubernetes.io/managed-by: {{.Release.Service}} + chartName: {{.Chart.Name}} + chartVersion: {{.Chart.Version | quote}} +spec: + restartPolicy: {{default "Never" .restart_policy}} + containers: + - name: waiter + image: "alpine:3.3" + command: ["/bin/sleep","9000"] diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/alpine/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest/alpine/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/signtest/templates/pod.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/templates/pod.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/signtest/templates/pod.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/signtest/templates/pod.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/signtest/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/signtest/values.yaml new file mode 100644 index 00000000..e69de29b diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/Chart.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/Chart.yaml diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml new file mode 100644 index 00000000..27501e1e --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/values.yaml new file mode 100644 index 00000000..f0381ae6 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartA/values.yaml @@ -0,0 +1,17 @@ +# Default values for subchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. +# subchartA +service: + name: apache + type: ClusterIP + externalPort: 80 + internalPort: 80 +SCAdata: + SCAbool: false + SCAfloat: 3.1 + SCAint: 55 + SCAstring: "jabba" + SCAnested1: + SCAnested2: true + diff --git a/pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/Chart.yaml similarity index 100% rename from pkg/helm/pkg/chartutil/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/Chart.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml new file mode 100644 index 00000000..27501e1e --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/charts/subchartB/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/charts/subchartB/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/crds/crdA.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/crds/crdA.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/crds/crdA.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/extra_values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/extra_values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/extra_values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/extra_values.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/NOTES.txt b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/NOTES.txt new file mode 100644 index 00000000..4bdf443f --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/NOTES.txt @@ -0,0 +1 @@ +Sample notes for {{ .Chart.Name }} \ No newline at end of file diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/service.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/service.yaml new file mode 100644 index 00000000..19c931cc --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/service.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Chart.Name }} + labels: + helm.sh/chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + app.kubernetes.io/instance: "{{ .Release.Name }}" + kube-version/major: "{{ .Capabilities.KubeVersion.Major }}" + kube-version/minor: "{{ .Capabilities.KubeVersion.Minor }}" + kube-version/version: "v{{ .Capabilities.KubeVersion.Major }}.{{ .Capabilities.KubeVersion.Minor }}.0" +{{- if .Capabilities.APIVersions.Has "helm.k8s.io/test" }} + kube-api-version/test: v1 +{{- end }} +{{- if .Capabilities.APIVersions.Has "helm.k8s.io/test2" }} + kube-api-version/test2: v2 +{{- end }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.externalPort }} + targetPort: {{ .Values.service.internalPort }} + protocol: TCP + name: {{ .Values.service.name }} + selector: + app.kubernetes.io/name: {{ .Chart.Name }} diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/configmap.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/configmap.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/configmap.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/role.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/subdir/role.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/role.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml new file mode 100644 index 00000000..5d193f1a --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/rolebinding.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ .Chart.Name }}-binding +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ .Chart.Name }}-role +subjects: +- kind: ServiceAccount + name: {{ .Chart.Name }}-sa + namespace: default diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml new file mode 100644 index 00000000..7126c7d8 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/subdir/serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }}-sa diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/tests/test-config.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/tests/test-config.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/tests/test-config.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/tests/test-nothing.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/templates/tests/test-nothing.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/templates/tests/test-nothing.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/subchart/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/subchart/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/subchart/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/subchart/values.yaml diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/test-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/test-0.1.0.tgz new file mode 100644 index 00000000..9ed772a7 Binary files /dev/null and b/pkg/helm/pkg/cmd/testdata/testcharts/test-0.1.0.tgz differ diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/test/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/test/Chart.yaml new file mode 100644 index 00000000..53e47c82 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/test/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Test chart for untar conflict testing +name: test +version: 0.1.0 diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/test/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/test/values.yaml new file mode 100644 index 00000000..2f01ba53 --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/test/values.yaml @@ -0,0 +1 @@ +# Default values for test diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/test1-0.1.0.tgz b/pkg/helm/pkg/cmd/testdata/testcharts/test1-0.1.0.tgz new file mode 100644 index 00000000..60e00324 Binary files /dev/null and b/pkg/helm/pkg/cmd/testdata/testcharts/test1-0.1.0.tgz differ diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/test1/Chart.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/test1/Chart.yaml new file mode 100644 index 00000000..3dc8fbbf --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/test1/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Test chart for untar conflict testing +name: test1 +version: 0.1.0 diff --git a/pkg/helm/pkg/cmd/testdata/testcharts/test1/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/test1/values.yaml new file mode 100644 index 00000000..823016ff --- /dev/null +++ b/pkg/helm/pkg/cmd/testdata/testcharts/test1/values.yaml @@ -0,0 +1,2 @@ +# Default values for test1# Default values for test1 + diff --git a/pkg/helm/cmd/helm/testdata/testcharts/upgradetest/templates/configmap.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/upgradetest/templates/configmap.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/upgradetest/templates/configmap.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/upgradetest/templates/configmap.yaml diff --git a/pkg/helm/cmd/helm/testdata/testcharts/upgradetest/values.yaml b/pkg/helm/pkg/cmd/testdata/testcharts/upgradetest/values.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testcharts/upgradetest/values.yaml rename to pkg/helm/pkg/cmd/testdata/testcharts/upgradetest/values.yaml diff --git a/pkg/helm/cmd/helm/testdata/testserver/index.yaml b/pkg/helm/pkg/cmd/testdata/testserver/index.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testserver/index.yaml rename to pkg/helm/pkg/cmd/testdata/testserver/index.yaml diff --git a/pkg/helm/cmd/helm/testdata/testserver/repository/repositories.yaml b/pkg/helm/pkg/cmd/testdata/testserver/repository/repositories.yaml similarity index 100% rename from pkg/helm/cmd/helm/testdata/testserver/repository/repositories.yaml rename to pkg/helm/pkg/cmd/testdata/testserver/repository/repositories.yaml diff --git a/pkg/helm/cmd/helm/verify.go b/pkg/helm/pkg/cmd/verify.go similarity index 83% rename from pkg/helm/cmd/helm/verify.go rename to pkg/helm/pkg/cmd/verify.go index d34da118..72cf7771 100644 --- a/pkg/helm/cmd/helm/verify.go +++ b/pkg/helm/pkg/cmd/verify.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package helm +package cmd import ( "fmt" @@ -21,8 +21,8 @@ import ( "github.com/spf13/cobra" - "github.com/werf/nelm/pkg/helm/cmd/helm/require" "github.com/werf/nelm/pkg/helm/pkg/action" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" ) const verifyDesc = ` @@ -44,21 +44,21 @@ func newVerifyCmd(out io.Writer) *cobra.Command { Short: "verify that a chart at the given path has been signed and is valid", Long: verifyDesc, Args: require.ExactArgs(1), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ValidArgsFunction: func(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) { if len(args) == 0 { // Allow file completion when completing the argument for the path return nil, cobra.ShellCompDirectiveDefault } // No more completions, so disable file completion - return nil, cobra.ShellCompDirectiveNoFileComp + return noMoreArgsComp() }, - RunE: func(cmd *cobra.Command, args []string) error { - err := client.Run(args[0]) + RunE: func(_ *cobra.Command, args []string) error { + result, err := client.Run(args[0]) if err != nil { return err } - fmt.Fprint(out, client.Out) + fmt.Fprint(out, result) return nil }, diff --git a/pkg/helm/pkg/cmd/verify_test.go b/pkg/helm/pkg/cmd/verify_test.go new file mode 100644 index 00000000..ae373afd --- /dev/null +++ b/pkg/helm/pkg/cmd/verify_test.go @@ -0,0 +1,97 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "runtime" + "testing" +) + +func TestVerifyCmd(t *testing.T) { + + statExe := "stat" + statPathMsg := "no such file or directory" + statFileMsg := statPathMsg + if runtime.GOOS == "windows" { + statExe = "FindFirstFile" + statPathMsg = "The system cannot find the path specified." + statFileMsg = "The system cannot find the file specified." + } + + tests := []struct { + name string + cmd string + expect string + wantError bool + }{ + { + name: "verify requires a chart", + cmd: "verify", + expect: "\"helm verify\" requires 1 argument\n\nUsage: helm verify PATH [flags]", + wantError: true, + }, + { + name: "verify requires that chart exists", + cmd: "verify no/such/file", + expect: fmt.Sprintf("%s no/such/file: %s", statExe, statPathMsg), + wantError: true, + }, + { + name: "verify requires that chart is not a directory", + cmd: "verify testdata/testcharts/signtest", + expect: "unpacked charts cannot be verified", + wantError: true, + }, + { + name: "verify requires that chart has prov file", + cmd: "verify testdata/testcharts/compressedchart-0.1.0.tgz", + expect: fmt.Sprintf("could not load provenance file testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s testdata/testcharts/compressedchart-0.1.0.tgz.prov: %s", statExe, statFileMsg), + wantError: true, + }, + { + name: "verify validates a properly signed chart", + cmd: "verify testdata/testcharts/signtest-0.1.0.tgz --keyring testdata/helm-test-key.pub", + expect: "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \nUsing Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\nChart Hash Verified: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\n", + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, out, err := executeActionCommand(tt.cmd) + if tt.wantError { + if err == nil { + t.Errorf("Expected error, but got none: %q", out) + } + if err.Error() != tt.expect { + t.Errorf("Expected error %q, got %q", tt.expect, err) + } + return + } else if err != nil { + t.Errorf("Unexpected error: %s", err) + } + if out != tt.expect { + t.Errorf("Expected %q, got %q", tt.expect, out) + } + }) + } +} + +func TestVerifyFileCompletion(t *testing.T) { + checkFileCompletion(t, "verify", true) + checkFileCompletion(t, "verify mypath", false) +} diff --git a/pkg/helm/pkg/cmd/version.go b/pkg/helm/pkg/cmd/version.go new file mode 100644 index 00000000..44843d95 --- /dev/null +++ b/pkg/helm/pkg/cmd/version.go @@ -0,0 +1,101 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "io" + "text/template" + + "github.com/spf13/cobra" + + "github.com/werf/nelm/pkg/helm/intern/version" + "github.com/werf/nelm/pkg/helm/pkg/cmd/require" +) + +const versionDesc = ` +Show the version for Helm. + +This will print a representation the version of Helm. +The output will look something like this: + +version.BuildInfo{Version:"v3.2.1", GitCommit:"fe51cd1e31e6a202cba7dead9552a6d418ded79a", GitTreeState:"clean", GoVersion:"go1.13.10"} + +- Version is the semantic version of the release. +- GitCommit is the SHA for the commit that this version was built from. +- GitTreeState is "clean" if there are no local code changes when this binary was + built, and "dirty" if the binary was built from locally modified code. +- GoVersion is the version of Go that was used to compile Helm. + +When using the --template flag the following properties are available to use in +the template: + +- .Version contains the semantic version of Helm +- .GitCommit is the git commit +- .GitTreeState is the state of the git tree when Helm was built +- .GoVersion contains the version of Go that Helm was compiled with + +For example, --template='Version: {{.Version}}' outputs 'Version: v3.2.1'. +` + +type versionOptions struct { + short bool + template string +} + +func newVersionCmd(out io.Writer) *cobra.Command { + o := &versionOptions{} + + cmd := &cobra.Command{ + Use: "version", + Short: "print the helm version information", + Long: versionDesc, + Args: require.NoArgs, + ValidArgsFunction: noMoreArgsCompFunc, + RunE: func(_ *cobra.Command, _ []string) error { + return o.run(out) + }, + } + f := cmd.Flags() + f.BoolVar(&o.short, "short", false, "print the version number") + f.StringVar(&o.template, "template", "", "template for version string format") + + return cmd +} + +func (o *versionOptions) run(out io.Writer) error { + if o.template != "" { + tt, err := template.New("_").Parse(o.template) + if err != nil { + return err + } + return tt.Execute(out, version.Get()) + } + fmt.Fprintln(out, formatVersion(o.short)) + return nil +} + +func formatVersion(short bool) string { + v := version.Get() + if short { + if len(v.GitCommit) >= 7 { + return fmt.Sprintf("%s+g%s", v.Version, v.GitCommit[:7]) + } + return version.GetVersion() + } + return fmt.Sprintf("%#v", v) +} diff --git a/pkg/helm/pkg/cmd/version_test.go b/pkg/helm/pkg/cmd/version_test.go new file mode 100644 index 00000000..9551de76 --- /dev/null +++ b/pkg/helm/pkg/cmd/version_test.go @@ -0,0 +1,41 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "testing" +) + +func TestVersion(t *testing.T) { + tests := []cmdTestCase{{ + name: "default", + cmd: "version", + golden: "output/version.txt", + }, { + name: "short", + cmd: "version --short", + golden: "output/version-short.txt", + }, { + name: "template", + cmd: "version --template='Version: {{.Version}}'", + golden: "output/version-template.txt", + }} + runTestCmd(t, tests) +} + +func TestVersionFileCompletion(t *testing.T) { + checkFileCompletion(t, "version", false) +} diff --git a/pkg/helm/pkg/downloader/cache.go b/pkg/helm/pkg/downloader/cache.go new file mode 100644 index 00000000..7d4f2858 --- /dev/null +++ b/pkg/helm/pkg/downloader/cache.go @@ -0,0 +1,89 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package downloader + +import ( + "crypto/sha256" + "errors" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + + "github.com/werf/nelm/pkg/helm/intern/fileutil" +) + +// Cache describes a cache that can get and put chart data. +// The cache key is the sha256 has of the content. sha256 is used in Helm for +// digests in index files providing a common key for checking content. +type Cache interface { + // Get returns a reader for the given key. + Get(key [sha256.Size]byte, cacheType string) (string, error) + // Put stores the given reader for the given key. + Put(key [sha256.Size]byte, data io.Reader, cacheType string) (string, error) +} + +// CacheChart specifies the content is a chart +var CacheChart = ".chart" + +// CacheProv specifies the content is a provenance file +var CacheProv = ".prov" + +// TODO: The cache assumes files because much of Helm assumes files. Convert +// Helm to pass content around instead of file locations. + +// DiskCache is a cache that stores data on disk. +type DiskCache struct { + Root string +} + +// Get returns a reader for the given key. +func (c *DiskCache) Get(key [sha256.Size]byte, cacheType string) (string, error) { + p := c.fileName(key, cacheType) + fi, err := os.Stat(p) + if err != nil { + return "", err + } + // Empty files treated as not exist because there is no content. + if fi.Size() == 0 { + return p, os.ErrNotExist + } + // directories should never happen unless something outside helm is operating + // on this content. + if fi.IsDir() { + return p, errors.New("is a directory") + } + return p, nil +} + +// Put stores the given reader for the given key. +// It returns the path to the stored file. +func (c *DiskCache) Put(key [sha256.Size]byte, data io.Reader, cacheType string) (string, error) { + // TODO: verify the key and digest of the key are the same. + p := c.fileName(key, cacheType) + if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { + slog.Error("failed to create cache directory") + return p, err + } + return p, fileutil.AtomicWriteFile(p, data, 0644) +} + +// fileName generates the filename in a structured manner where the first part is the +// directory and the full hash is the filename. +func (c *DiskCache) fileName(id [sha256.Size]byte, cacheType string) string { + return filepath.Join(c.Root, fmt.Sprintf("%02x", id[0]), fmt.Sprintf("%x", id)+cacheType) +} diff --git a/pkg/helm/pkg/downloader/cache_test.go b/pkg/helm/pkg/downloader/cache_test.go new file mode 100644 index 00000000..340c77ab --- /dev/null +++ b/pkg/helm/pkg/downloader/cache_test.go @@ -0,0 +1,122 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package downloader + +import ( + "bytes" + "crypto/sha256" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// compiler check to ensure DiskCache implements the Cache interface. +var _ Cache = (*DiskCache)(nil) + +func TestDiskCache_PutAndGet(t *testing.T) { + // Setup a temporary directory for the cache + tmpDir := t.TempDir() + cache := &DiskCache{Root: tmpDir} + + // Test data + content := []byte("hello world") + key := sha256.Sum256(content) + + // --- Test case 1: Put and Get a regular file (prov=false) --- + t.Run("PutAndGetTgz", func(t *testing.T) { + // Put the data into the cache + path, err := cache.Put(key, bytes.NewReader(content), CacheChart) + require.NoError(t, err, "Put should not return an error") + + // Verify the file exists at the returned path + _, err = os.Stat(path) + require.NoError(t, err, "File should exist after Put") + + // Get the file from the cache + retrievedPath, err := cache.Get(key, CacheChart) + require.NoError(t, err, "Get should not return an error for existing file") + assert.Equal(t, path, retrievedPath, "Get should return the same path as Put") + + // Verify content + data, err := os.ReadFile(retrievedPath) + require.NoError(t, err) + assert.Equal(t, content, data, "Content of retrieved file should match original content") + }) + + // --- Test case 2: Put and Get a provenance file (prov=true) --- + t.Run("PutAndGetProv", func(t *testing.T) { + provContent := []byte("provenance data") + provKey := sha256.Sum256(provContent) + + path, err := cache.Put(provKey, bytes.NewReader(provContent), CacheProv) + require.NoError(t, err) + + retrievedPath, err := cache.Get(provKey, CacheProv) + require.NoError(t, err) + assert.Equal(t, path, retrievedPath) + + data, err := os.ReadFile(retrievedPath) + require.NoError(t, err) + assert.Equal(t, provContent, data) + }) + + // --- Test case 3: Get a non-existent file --- + t.Run("GetNonExistent", func(t *testing.T) { + nonExistentKey := sha256.Sum256([]byte("does not exist")) + _, err := cache.Get(nonExistentKey, CacheChart) + assert.ErrorIs(t, err, os.ErrNotExist, "Get for a non-existent key should return os.ErrNotExist") + }) + + // --- Test case 4: Put an empty file --- + t.Run("PutEmptyFile", func(t *testing.T) { + emptyContent := []byte{} + emptyKey := sha256.Sum256(emptyContent) + + path, err := cache.Put(emptyKey, bytes.NewReader(emptyContent), CacheChart) + require.NoError(t, err) + + // Get should return ErrNotExist for empty files + _, err = cache.Get(emptyKey, CacheChart) + assert.ErrorIs(t, err, os.ErrNotExist, "Get for an empty file should return os.ErrNotExist") + + // But the file should exist + _, err = os.Stat(path) + require.NoError(t, err, "Empty file should still exist on disk") + }) + + // --- Test case 5: Get a directory --- + t.Run("GetDirectory", func(t *testing.T) { + dirKey := sha256.Sum256([]byte("i am a directory")) + dirPath := cache.fileName(dirKey, CacheChart) + err := os.MkdirAll(dirPath, 0755) + require.NoError(t, err) + + _, err = cache.Get(dirKey, CacheChart) + assert.EqualError(t, err, "is a directory") + }) +} + +func TestDiskCache_fileName(t *testing.T) { + cache := &DiskCache{Root: "/tmp/cache"} + key := sha256.Sum256([]byte("some data")) + + assert.Equal(t, filepath.Join("/tmp/cache", "13", "1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee.chart"), cache.fileName(key, CacheChart)) + assert.Equal(t, filepath.Join("/tmp/cache", "13", "1307990e6ba5ca145eb35e99182a9bec46531bc54ddf656a602c780fa0240dee.prov"), cache.fileName(key, CacheProv)) +} diff --git a/pkg/helm/pkg/downloader/chart_downloader.go b/pkg/helm/pkg/downloader/chart_downloader.go index 27ab6b6a..80ddd6e3 100644 --- a/pkg/helm/pkg/downloader/chart_downloader.go +++ b/pkg/helm/pkg/downloader/chart_downloader.go @@ -16,23 +16,27 @@ limitations under the License. package downloader import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "errors" "fmt" "io" + "io/fs" + "log/slog" "net/url" "os" "path/filepath" "strings" - "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/intern/fileutil" + ifs "github.com/werf/nelm/pkg/helm/intern/third_party/dep/fs" "github.com/werf/nelm/pkg/helm/intern/urlutil" "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/provenance" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/repo" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) // VerificationStrategy describes a strategy for determining whether to verify a chart. @@ -73,6 +77,14 @@ type ChartDownloader struct { RegistryClient *registry.Client RepositoryConfig string RepositoryCache string + + // ContentCache is the location where Cache stores its files by default + // In previous versions of Helm the charts were put in the RepositoryCache. The + // repositories and charts are stored in 2 different caches. + ContentCache string + + // Cache specifies the cache implementation to use. + Cache Cache } // DownloadTo retrieves a chart. Depending on the settings, it may also download a provenance file. @@ -87,7 +99,14 @@ type ChartDownloader struct { // Returns a string path to the location where the file was downloaded and a verification // (if provenance was verified), or an error if something bad happened. func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *provenance.Verification, error) { - u, err := c.ResolveChartVersion(ref, version) + if c.Cache == nil { + if c.ContentCache == "" { + return "", nil, errors.New("content cache must be set") + } + c.Cache = &DiskCache{Root: c.ContentCache} + slog.Debug("set up default downloader cache") + } + hash, u, err := c.ResolveChartVersion(ref, version) if err != nil { return "", nil, err } @@ -97,9 +116,42 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return "", nil, err } - data, err := g.Get(u.String(), c.Options...) - if err != nil { - return "", nil, err + // Check the cache for the content. Otherwise download it. + // Note, this process will pull from the cache but does not automatically populate + // the cache with the file it downloads. + var data *bytes.Buffer + var found bool + var digest []byte + var digest32 [32]byte + if hash != "" { + // if there is a hash, populate the other formats + // Strip the algorithm prefix (e.g., "sha256:") if present + digest, err = hex.DecodeString(stripDigestAlgorithm(hash)) + if err != nil { + return "", nil, err + } + if len(digest) != 32 { + return "", nil, fmt.Errorf("invalid digest length: %d", len(digest)) + } + + copy(digest32[:], digest) + if pth, err := c.Cache.Get(digest32, CacheChart); err == nil { + fdata, err := os.ReadFile(pth) + if err == nil { + found = true + data = bytes.NewBuffer(fdata) + slog.Debug("found chart in cache", "id", hash) + } + } + } + + if !found { + c.Options = append(c.Options, getter.WithAcceptHeader("application/gzip,application/octet-stream")) + + data, err = g.Get(u.String(), c.Options...) + if err != nil { + return "", nil, err + } } name := filepath.Base(u.Path) @@ -109,28 +161,48 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven } destfile := filepath.Join(dest, name) - if err := fileutil.AtomicWriteFile(destfile, data, 0644); err != nil { + + // Use PlatformAtomicWriteFile to handle platform-specific concurrency concerns + // (Windows requires locking to avoid "Access Denied" errors when multiple + // processes write the same file) + if err := fileutil.PlatformAtomicWriteFile(destfile, data, 0644); err != nil { return destfile, nil, err } // If provenance is requested, verify it. ver := &provenance.Verification{} if c.Verify > VerifyNever { - body, err := g.Get(u.String() + ".prov") - if err != nil { - if c.Verify == VerifyAlways { - return destfile, ver, errors.Errorf("failed to fetch provenance %q", u.String()+".prov") + found = false + var body *bytes.Buffer + if hash != "" { + if pth, err := c.Cache.Get(digest32, CacheProv); err == nil { + fdata, err := os.ReadFile(pth) + if err == nil { + found = true + body = bytes.NewBuffer(fdata) + slog.Debug("found provenance in cache", "id", hash) + } + } + } + if !found { + body, err = g.Get(u.String() + ".prov") + if err != nil { + if c.Verify == VerifyAlways { + return destfile, ver, fmt.Errorf("failed to fetch provenance %q", u.String()+".prov") + } + fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) + return destfile, ver, nil } - fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) - return destfile, ver, nil } provfile := destfile + ".prov" - if err := fileutil.AtomicWriteFile(provfile, body, 0644); err != nil { + + // Use PlatformAtomicWriteFile for the provenance file as well + if err := fileutil.PlatformAtomicWriteFile(provfile, body, 0644); err != nil { return destfile, nil, err } if c.Verify != VerifyLater { - ver, err = VerifyChart(destfile, c.Keyring) + ver, err = VerifyChart(destfile, destfile+".prov", c.Keyring) if err != nil { // Fail always in this case, since it means the verification step // failed. @@ -141,43 +213,144 @@ func (c *ChartDownloader) DownloadTo(ref, version, dest string) (string, *proven return destfile, ver, nil } -func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, error) { - var tag string - var err error - - // Evaluate whether an explicit version has been provided. Otherwise, determine version to use - _, errSemVer := semver.NewVersion(version) - if errSemVer == nil { - tag = version - } else { - // Retrieve list of repository tags - tags, err := c.RegistryClient.Tags(strings.TrimPrefix(ref, fmt.Sprintf("%s://", registry.OCIScheme))) - if err != nil { - return nil, err +// DownloadToCache retrieves resources while using a content based cache. +func (c *ChartDownloader) DownloadToCache(ref, version string) (string, *provenance.Verification, error) { + if c.Cache == nil { + if c.ContentCache == "" { + return "", nil, errors.New("content cache must be set") + } + c.Cache = &DiskCache{Root: c.ContentCache} + slog.Debug("set up default downloader cache") + } + + digestString, u, err := c.ResolveChartVersion(ref, version) + if err != nil { + return "", nil, err + } + + g, err := c.Getters.ByScheme(u.Scheme) + if err != nil { + return "", nil, err + } + + c.Options = append(c.Options, getter.WithAcceptHeader("application/gzip,application/octet-stream")) + + // Check the cache for the file + // Strip the algorithm prefix (e.g., "sha256:") if present + digest, err := hex.DecodeString(stripDigestAlgorithm(digestString)) + if err != nil { + return "", nil, fmt.Errorf("unable to decode digest: %w", err) + } + if digestString != "" && len(digest) != 32 { + return "", nil, fmt.Errorf("invalid digest length: %d", len(digest)) + } + var digest32 [32]byte + copy(digest32[:], digest) + + var pth string + // only fetch from the cache if we have a digest + if len(digest) > 0 { + pth, err = c.Cache.Get(digest32, CacheChart) + if err == nil { + slog.Debug("found chart in cache", "id", digestString) + } + } + if len(digest) == 0 || err != nil { + slog.Debug("attempting to download chart", "ref", ref, "version", version) + if err != nil && !os.IsNotExist(err) { + return "", nil, err } - if len(tags) == 0 { - return nil, errors.Errorf("Unable to locate any tags in provided repository: %s", ref) + + // Get file not in the cache + data, gerr := g.Get(u.String(), c.Options...) + if gerr != nil { + return "", nil, gerr + } + + // Generate the digest + if len(digest) == 0 { + digest32 = sha256.Sum256(data.Bytes()) } - // Determine if version provided - // If empty, try to get the highest available tag - // If exact version, try to find it - // If semver constraint string, try to find a match - tag, err = registry.GetTagMatchingVersionOrConstraint(tags, version) + pth, err = c.Cache.Put(digest32, data, CacheChart) if err != nil { - return nil, err + return "", nil, err } + slog.Debug("put downloaded chart in cache", "id", hex.EncodeToString(digest32[:])) } - u.Path = fmt.Sprintf("%s:%s", u.Path, tag) + // If provenance is requested, verify it. + ver := &provenance.Verification{} + if c.Verify > VerifyNever { + + ppth, err := c.Cache.Get(digest32, CacheProv) + if err == nil { + slog.Debug("found provenance in cache", "id", digestString) + } else { + if !os.IsNotExist(err) { + return pth, ver, err + } + + body, err := g.Get(u.String() + ".prov") + if err != nil { + if c.Verify == VerifyAlways { + return pth, ver, fmt.Errorf("failed to fetch provenance %q", u.String()+".prov") + } + fmt.Fprintf(c.Out, "WARNING: Verification not found for %s: %s\n", ref, err) + return pth, ver, nil + } + + ppth, err = c.Cache.Put(digest32, body, CacheProv) + if err != nil { + return "", nil, err + } + slog.Debug("put downloaded provenance file in cache", "id", hex.EncodeToString(digest32[:])) + } + + if c.Verify != VerifyLater { + + // provenance files pin to a specific name so this needs to be accounted for + // when verifying. + // Note, this does make an assumption that the name/version is unique to a + // hash when a provenance file is used. If this isn't true, this section of code + // will need to be reworked. + name := filepath.Base(u.Path) + if u.Scheme == registry.OCIScheme { + idx := strings.LastIndexByte(name, ':') + name = fmt.Sprintf("%s-%s.tgz", name[:idx], name[idx+1:]) + } + + // Copy chart to a known location with the right name for verification and then + // clean it up. + tmpdir := filepath.Dir(filepath.Join(c.ContentCache, "tmp")) + if err := os.MkdirAll(tmpdir, 0755); err != nil { + return pth, ver, err + } + tmpfile := filepath.Join(tmpdir, name) + err = ifs.CopyFile(pth, tmpfile) + if err != nil { + return pth, ver, err + } + // Not removing the tmp dir itself because a concurrent process may be using it + defer os.RemoveAll(tmpfile) - return u, err + ver, err = VerifyChart(tmpfile, ppth, c.Keyring) + if err != nil { + // Fail always in this case, since it means the verification step + // failed. + return pth, ver, err + } + } + } + return pth, ver, nil } // ResolveChartVersion resolves a chart reference to a URL. // -// It returns the URL and sets the ChartDownloader's Options that can fetch -// the URL using the appropriate Getter. +// It returns: +// - A hash of the content if available +// - The URL and sets the ChartDownloader's Options that can fetch the URL using the appropriate Getter. +// - An error if there is one // // A reference may be an HTTP URL, an oci reference URL, a 'reponame/chartname' // reference, or a local path. @@ -189,19 +362,26 @@ func (c *ChartDownloader) getOciURI(ref, version string, u *url.URL) (*url.URL, // - If version is non-empty, this will return the URL for that version // - If version is empty, this will return the URL for the latest version // - If no version can be found, an error is returned -func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, error) { +// +// TODO: support OCI hash +func (c *ChartDownloader) ResolveChartVersion(ref, version string) (string, *url.URL, error) { u, err := url.Parse(ref) if err != nil { - return nil, errors.Errorf("invalid chart URL format: %s", ref) + return "", nil, fmt.Errorf("invalid chart URL format: %s", ref) } if registry.IsOCI(u.String()) { - return c.getOciURI(ref, version, u) + if c.RegistryClient == nil { + return "", nil, fmt.Errorf("unable to lookup ref %s at version '%s', missing registry client", ref, version) + } + + digest, OCIref, err := c.RegistryClient.ValidateReference(ref, version, u) + return digest, OCIref, err } rf, err := loadRepoConfig(c.RepositoryConfig) if err != nil { - return u, err + return "", u, err } if u.IsAbs() && len(u.Host) > 0 && len(u.Path) > 0 { @@ -218,9 +398,9 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er if err == ErrNoOwnerRepo { // Make sure to add the ref URL as the URL for the getter c.Options = append(c.Options, getter.WithURL(ref)) - return u, nil + return "", u, nil } - return u, err + return "", u, err } // If we get here, we don't need to go through the next phase of looking @@ -239,21 +419,20 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er getter.WithPassCredentialsAll(rc.PassCredentialsAll), ) } - return u, nil + return "", u, nil } // See if it's of the form: repo/path_to_chart p := strings.SplitN(u.Path, "/", 2) if len(p) < 2 { - return u, errors.Errorf("non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) + return "", u, fmt.Errorf("non-absolute URLs should be in form of repo_name/path_to_chart, got: %s", u) } repoName := p[0] chartName := p[1] rc, err := pickChartRepositoryConfigByName(repoName, rf.Repositories) - if err != nil { - return u, err + return "", u, err } // Now that we have the chart repository information we can use that URL @@ -262,7 +441,7 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er r, err := repo.NewChartRepository(rc, c.Getters) if err != nil { - return u, err + return "", u, err } if r != nil && r.Config != nil { @@ -281,33 +460,33 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name)) i, err := repo.LoadIndexFile(idxFile) if err != nil { - return u, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") + return "", u, fmt.Errorf("no cached repo found. (try 'helm repo update'): %w", err) } cv, err := i.Get(chartName, version) if err != nil { - return u, errors.Wrapf(err, "chart %q matching %s not found in %s index. (try 'helm repo update')", chartName, version, r.Config.Name) + return "", u, fmt.Errorf("chart %q matching %s not found in %s index. (try 'helm repo update'): %w", chartName, version, r.Config.Name, err) } if len(cv.URLs) == 0 { - return u, errors.Errorf("chart %q has no downloadable URLs", ref) + return "", u, fmt.Errorf("chart %q has no downloadable URLs", ref) } // TODO: Seems that picking first URL is not fully correct resolvedURL, err := repo.ResolveReferenceURL(rc.URL, cv.URLs[0]) - if err != nil { - return u, errors.Errorf("invalid chart URL format: %s", ref) + return cv.Digest, u, fmt.Errorf("invalid chart URL format: %s", ref) } - return url.Parse(resolvedURL) + loc, err := url.Parse(resolvedURL) + return cv.Digest, loc, err } // VerifyChart takes a path to a chart archive and a keyring, and verifies the chart. // // It assumes that a chart archive file is accompanied by a provenance file whose // name is the archive file name plus the ".prov" extension. -func VerifyChart(path, keyring string) (*provenance.Verification, error) { +func VerifyChart(path, provfile, keyring string) (*provenance.Verification, error) { // For now, error out if it's not a tar file. switch fi, err := os.Stat(path); { case err != nil: @@ -318,16 +497,26 @@ func VerifyChart(path, keyring string) (*provenance.Verification, error) { return nil, errors.New("chart must be a tgz file") } - provfile := path + ".prov" if _, err := os.Stat(provfile); err != nil { - return nil, errors.Wrapf(err, "could not load provenance file %s", provfile) + return nil, fmt.Errorf("could not load provenance file %s: %w", provfile, err) } sig, err := provenance.NewFromKeyring(keyring, "") if err != nil { - return nil, errors.Wrap(err, "failed to load keyring") + return nil, fmt.Errorf("failed to load keyring: %w", err) + } + + // Read archive and provenance files + archiveData, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read chart archive: %w", err) + } + provData, err := os.ReadFile(provfile) + if err != nil { + return nil, fmt.Errorf("failed to read provenance file: %w", err) } - return sig.Verify(path, provfile) + + return sig.Verify(archiveData, provData, filepath.Base(path)) } // isTar tests whether the given file is a tar file. @@ -342,12 +531,12 @@ func pickChartRepositoryConfigByName(name string, cfgs []*repo.Entry) (*repo.Ent for _, rc := range cfgs { if rc.Name == name { if rc.URL == "" { - return nil, errors.Errorf("no URL found for repository %s", name) + return nil, fmt.Errorf("no URL found for repository %s", name) } return rc, nil } } - return nil, errors.Errorf("repo %s not found", name) + return nil, fmt.Errorf("repo %s not found", name) } // scanReposForURL scans all repos to find which repo contains the given URL. @@ -380,7 +569,7 @@ func (c *ChartDownloader) scanReposForURL(u string, rf *repo.File) (*repo.Entry, idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name)) i, err := repo.LoadIndexFile(idxFile) if err != nil { - return nil, errors.Wrap(err, "no cached repo found. (try 'helm repo update')") + return nil, fmt.Errorf("no cached repo found. (try 'helm repo update'): %w", err) } for _, entry := range i.Entries { @@ -399,32 +588,17 @@ func (c *ChartDownloader) scanReposForURL(u string, rf *repo.File) (*repo.Entry, func loadRepoConfig(file string) (*repo.File, error) { r, err := repo.LoadFile(file) - if err != nil && !os.IsNotExist(errors.Cause(err)) { + if err != nil && !errors.Is(err, fs.ErrNotExist) { return nil, err } return r, nil } -type VerificationStrategyString string - -const ( - VerificationStrategyStringNever VerificationStrategyString = "never" - VerificationStrategyStringIfPossible VerificationStrategyString = "if-possible" - VerificationStrategyStringAlways VerificationStrategyString = "always" - VerificationStrategyStringLater VerificationStrategyString = "later" -) - -func (s VerificationStrategyString) ToVerificationStrategy() VerificationStrategy { - switch s { - case VerificationStrategyStringNever: - return VerifyNever - case VerificationStrategyStringIfPossible: - return VerifyIfPossible - case VerificationStrategyStringAlways: - return VerifyAlways - case VerificationStrategyStringLater: - return VerifyLater - default: - panic("unknown VerificationStrategyString value") +// stripDigestAlgorithm removes the algorithm prefix (e.g., "sha256:") from a digest string. +// If no prefix is present, the original string is returned unchanged. +func stripDigestAlgorithm(digest string) string { + if idx := strings.Index(digest, ":"); idx >= 0 { + return digest[idx+1:] } + return digest } diff --git a/pkg/helm/pkg/downloader/chart_downloader_test.go b/pkg/helm/pkg/downloader/chart_downloader_test.go index 7a8c4f48..f8bdbfbf 100644 --- a/pkg/helm/pkg/downloader/chart_downloader_test.go +++ b/pkg/helm/pkg/downloader/chart_downloader_test.go @@ -16,15 +16,21 @@ limitations under the License. package downloader import ( + "crypto/sha256" + "encoding/hex" "os" "path/filepath" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/werf/nelm/pkg/helm/intern/test/ensure" "github.com/werf/nelm/pkg/helm/pkg/cli" "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" + "github.com/werf/nelm/pkg/helm/pkg/registry" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" ) const ( @@ -46,6 +52,7 @@ func TestResolveChartRef(t *testing.T) { {name: "reference, querystring repo", ref: "testing-querystring/alpine", expect: "http://example.com/alpine-1.2.3.tgz?key=value"}, {name: "reference, testing-relative repo", ref: "testing-relative/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"}, {name: "reference, testing-relative repo", ref: "testing-relative/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, + {name: "reference, testing-relative repo", ref: "testing-relative/baz", expect: "http://example.com/path/to/baz-1.2.3.tgz"}, {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/foo", expect: "http://example.com/helm/charts/foo-1.2.3.tgz"}, {name: "reference, testing-relative-trailing-slash repo", ref: "testing-relative-trailing-slash/bar", expect: "http://example.com/helm/bar-1.2.3.tgz"}, {name: "encoded URL", ref: "encoded-url/foobar", expect: "http://example.com/with%2Fslash/charts/foobar-4.2.1.tgz"}, @@ -53,12 +60,23 @@ func TestResolveChartRef(t *testing.T) { {name: "full URL, file", ref: "file:///foo-1.2.3.tgz", fail: true}, {name: "invalid", ref: "invalid-1.2.3", fail: true}, {name: "not found", ref: "nosuchthing/invalid-1.2.3", fail: true}, + {name: "ref with tag", ref: "oci://example.com/helm-charts/nginx:15.4.2", expect: "oci://example.com/helm-charts/nginx:15.4.2"}, + {name: "no repository", ref: "oci://", fail: true}, + {name: "oci ref", ref: "oci://example.com/helm-charts/nginx", version: "15.4.2", expect: "oci://example.com/helm-charts/nginx:15.4.2"}, + {name: "oci ref with sha256 and version mismatch", ref: "oci://example.com/install/by/sha:0.1.1@sha256:d234555386402a5867ef0169fefe5486858b6d8d209eaf32fd26d29b16807fd6", version: "0.1.2", fail: true}, + } + + // Create a mock registry client for OCI references + registryClient, err := registry.NewClient() + if err != nil { + t.Fatal(err) } c := ChartDownloader{ Out: os.Stderr, RepositoryConfig: repoConfig, RepositoryCache: repoCache, + RegistryClient: registryClient, Getters: getter.All(&cli.EnvSettings{ RepositoryConfig: repoConfig, RepositoryCache: repoCache, @@ -66,7 +84,7 @@ func TestResolveChartRef(t *testing.T) { } for _, tt := range tests { - u, err := c.ResolveChartVersion(tt.ref, tt.version) + _, u, err := c.ResolveChartVersion(tt.ref, tt.version) if err != nil { if tt.fail { continue @@ -118,7 +136,7 @@ func TestResolveChartOpts(t *testing.T) { continue } - u, err := c.ResolveChartVersion(tt.ref, tt.version) + _, u, err := c.ResolveChartVersion(tt.ref, tt.version) if err != nil { t.Errorf("%s: failed with error %s", tt.name, err) continue @@ -142,7 +160,7 @@ func TestResolveChartOpts(t *testing.T) { } func TestVerifyChart(t *testing.T) { - v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") + v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/signtest-0.1.0.tgz.prov", "testdata/helm-test-key.pub") if err != nil { t.Fatal(err) } @@ -171,7 +189,11 @@ func TestIsTar(t *testing.T) { } func TestDownloadTo(t *testing.T) { - srv := repotest.NewTempServerWithCleanupAndBasicAuth(t, "testdata/*.tgz*") + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + repotest.WithMiddleware(repotest.BasicAuthMiddleware(t)), + ) defer srv.Stop() if err := srv.CreateIndex(); err != nil { t.Fatal(err) @@ -181,15 +203,19 @@ func TestDownloadTo(t *testing.T) { t.Fatal(err) } + contentCache := t.TempDir() + c := ChartDownloader{ Out: os.Stderr, Verify: VerifyAlways, Keyring: "testdata/helm-test-key.pub", RepositoryConfig: repoConfig, RepositoryCache: repoCache, + ContentCache: contentCache, Getters: getter.All(&cli.EnvSettings{ RepositoryConfig: repoConfig, RepositoryCache: repoCache, + ContentCache: contentCache, }), Options: []getter.Option{ getter.WithBasicAuth("username", "password"), @@ -218,12 +244,11 @@ func TestDownloadTo(t *testing.T) { func TestDownloadTo_TLS(t *testing.T) { // Set up mock server w/ tls enabled - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") - srv.Stop() - if err != nil { - t.Fatal(err) - } - srv.StartTLS() + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + repotest.WithTLSConfig(repotest.MakeTestTLSConfig(t, "../../testdata")), + ) defer srv.Stop() if err := srv.CreateIndex(); err != nil { t.Fatal(err) @@ -234,6 +259,7 @@ func TestDownloadTo_TLS(t *testing.T) { repoConfig := filepath.Join(srv.Root(), "repositories.yaml") repoCache := srv.Root() + contentCache := t.TempDir() c := ChartDownloader{ Out: os.Stderr, @@ -241,11 +267,19 @@ func TestDownloadTo_TLS(t *testing.T) { Keyring: "testdata/helm-test-key.pub", RepositoryConfig: repoConfig, RepositoryCache: repoCache, + ContentCache: contentCache, Getters: getter.All(&cli.EnvSettings{ RepositoryConfig: repoConfig, RepositoryCache: repoCache, + ContentCache: contentCache, }), - Options: []getter.Option{}, + Options: []getter.Option{ + getter.WithTLSClientConfig( + "", + "", + filepath.Join("../../testdata/rootca.crt"), + ), + }, } cname := "test/signtest" dest := srv.Root() @@ -274,23 +308,26 @@ func TestDownloadTo_VerifyLater(t *testing.T) { dest := t.TempDir() // Set up a fake repo - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + ) defer srv.Stop() if err := srv.LinkIndices(); err != nil { t.Fatal(err) } + contentCache := t.TempDir() c := ChartDownloader{ Out: os.Stderr, Verify: VerifyLater, RepositoryConfig: repoConfig, RepositoryCache: repoCache, + ContentCache: contentCache, Getters: getter.All(&cli.EnvSettings{ RepositoryConfig: repoConfig, RepositoryCache: repoCache, + ContentCache: contentCache, }), } cname := "/signtest-0.1.0.tgz" @@ -344,3 +381,139 @@ func TestScanReposForURL(t *testing.T) { t.Fatalf("expected ErrNoOwnerRepo, got %v", err) } } + +func TestDownloadToCache(t *testing.T) { + srv := repotest.NewTempServer(t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + ) + defer srv.Stop() + if err := srv.CreateIndex(); err != nil { + t.Fatal(err) + } + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + // The repo file needs to point to our server. + repoFile := filepath.Join(srv.Root(), "repositories.yaml") + repoCache := srv.Root() + contentCache := t.TempDir() + + c := ChartDownloader{ + Out: os.Stderr, + Verify: VerifyNever, + RepositoryConfig: repoFile, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoFile, + RepositoryCache: repoCache, + ContentCache: contentCache, + }), + Cache: &DiskCache{Root: contentCache}, + } + + // Case 1: Chart not in cache, download it. + t.Run("download and cache chart", func(t *testing.T) { + // Clear cache for this test + os.RemoveAll(contentCache) + os.MkdirAll(contentCache, 0755) + c.Cache = &DiskCache{Root: contentCache} + + pth, v, err := c.DownloadToCache("test/signtest", "0.1.0") + require.NoError(t, err) + require.NotNil(t, v) + + // Check that the file exists at the returned path + _, err = os.Stat(pth) + require.NoError(t, err, "chart should exist at returned path") + + // Check that it's in the cache + digest, _, err := c.ResolveChartVersion("test/signtest", "0.1.0") + require.NoError(t, err) + digestBytes, err := hex.DecodeString(digest) + require.NoError(t, err) + var digestArray [sha256.Size]byte + copy(digestArray[:], digestBytes) + + cachePath, err := c.Cache.Get(digestArray, CacheChart) + require.NoError(t, err, "chart should now be in cache") + require.Equal(t, pth, cachePath) + }) + + // Case 2: Chart is in cache, get from cache. + t.Run("get chart from cache", func(t *testing.T) { + // The cache should be populated from the previous test. + // To prove it's coming from cache, we can stop the server. + // But repotest doesn't support restarting. + // Let's just call it again and assume it works if it's fast and doesn't error. + pth, v, err := c.DownloadToCache("test/signtest", "0.1.0") + require.NoError(t, err) + require.NotNil(t, v) + + _, err = os.Stat(pth) + require.NoError(t, err, "chart should exist at returned path") + }) + + // Case 3: Download with verification + t.Run("download and verify", func(t *testing.T) { + // Clear cache + os.RemoveAll(contentCache) + os.MkdirAll(contentCache, 0755) + c.Cache = &DiskCache{Root: contentCache} + c.Verify = VerifyAlways + c.Keyring = "testdata/helm-test-key.pub" + + _, v, err := c.DownloadToCache("test/signtest", "0.1.0") + require.NoError(t, err) + require.NotNil(t, v) + require.NotEmpty(t, v.FileHash, "verification should have a file hash") + + // Check that both chart and prov are in cache + digest, _, err := c.ResolveChartVersion("test/signtest", "0.1.0") + require.NoError(t, err) + digestBytes, err := hex.DecodeString(digest) + require.NoError(t, err) + var digestArray [sha256.Size]byte + copy(digestArray[:], digestBytes) + + _, err = c.Cache.Get(digestArray, CacheChart) + require.NoError(t, err, "chart should be in cache") + _, err = c.Cache.Get(digestArray, CacheProv) + require.NoError(t, err, "provenance file should be in cache") + + // Reset for other tests + c.Verify = VerifyNever + c.Keyring = "" + }) +} + +func TestStripDigestAlgorithm(t *testing.T) { + tests := map[string]struct { + input string + expected string + }{ + "sha256 prefixed digest": { + input: "sha256:aef46c66a7f2d5a12a7e3f54a64790daf5c9a9e66af3f46955efdaa6c900341d", + expected: "aef46c66a7f2d5a12a7e3f54a64790daf5c9a9e66af3f46955efdaa6c900341d", + }, + "sha512 prefixed digest": { + input: "sha512:abcdef1234567890", + expected: "abcdef1234567890", + }, + "plain hex digest without prefix": { + input: "aef46c66a7f2d5a12a7e3f54a64790daf5c9a9e66af3f46955efdaa6c900341d", + expected: "aef46c66a7f2d5a12a7e3f54a64790daf5c9a9e66af3f46955efdaa6c900341d", + }, + "empty string": { + input: "", + expected: "", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + result := stripDigestAlgorithm(tt.input) + assert.Equalf(t, tt.expected, result, "stripDigestAlgorithm(%q) = %q, want %q", tt.input, result, tt.expected) + }) + } +} diff --git a/pkg/helm/pkg/downloader/chart_downloader_windows_test.go b/pkg/helm/pkg/downloader/chart_downloader_windows_test.go new file mode 100644 index 00000000..734dd8e4 --- /dev/null +++ b/pkg/helm/pkg/downloader/chart_downloader_windows_test.go @@ -0,0 +1,131 @@ +//go:build windows + +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package downloader + +import ( + "os" + "path/filepath" + "sync" + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/cli" + "github.com/werf/nelm/pkg/helm/pkg/getter" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" +) + +// TestParallelDownloadTo tests that parallel downloads to the same file +// don't cause "Access Denied" errors on Windows. This test is Windows-specific +// because the file locking behavior is only needed on Windows. +func TestParallelDownloadTo(t *testing.T) { + // Set up a simple test server with a chart + srv := repotest.NewTempServer(t, repotest.WithChartSourceGlob("testdata/*.tgz")) + defer srv.Stop() + + if err := srv.CreateIndex(); err != nil { + t.Fatal(err) + } + + dest := t.TempDir() + cacheDir := t.TempDir() + + c := ChartDownloader{ + Out: os.Stderr, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + ContentCache: cacheDir, + Cache: &DiskCache{Root: cacheDir}, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + ContentCache: cacheDir, + }), + } + + // Use a direct URL to bypass repository lookup + chartURL := srv.URL() + "/local-subchart-0.1.0.tgz" + + // Number of parallel downloads to attempt + numDownloads := 10 + var wg sync.WaitGroup + errors := make([]error, numDownloads) + + // Launch multiple goroutines to download the same chart simultaneously + for i := 0; i < numDownloads; i++ { + wg.Add(1) + go func(index int) { + defer wg.Done() + _, _, err := c.DownloadTo(chartURL, "", dest) + errors[index] = err + }(i) + } + + wg.Wait() + + // Check if any download failed + failedCount := 0 + for i, err := range errors { + if err != nil { + t.Logf("Download %d failed: %v", i, err) + failedCount++ + } + } + + // With the file locking fix, all parallel downloads should succeed + if failedCount > 0 { + t.Errorf("Parallel downloads failed: %d out of %d downloads failed due to concurrent file access", failedCount, numDownloads) + } + + // Verify the file exists and is valid + expectedFile := filepath.Join(dest, "local-subchart-0.1.0.tgz") + info, err := os.Stat(expectedFile) + if err != nil { + t.Errorf("Expected file %s does not exist: %v", expectedFile, err) + } else { + // Verify the file is not empty + if info.Size() == 0 { + t.Errorf("Downloaded file %s is empty (0 bytes)", expectedFile) + } + + // Verify the file has the expected size (should match the source file) + sourceFile := "testdata/local-subchart-0.1.0.tgz" + sourceInfo, err := os.Stat(sourceFile) + if err == nil && info.Size() != sourceInfo.Size() { + t.Errorf("Downloaded file size (%d bytes) doesn't match source file size (%d bytes)", + info.Size(), sourceInfo.Size()) + } + + // Verify it's a valid tar.gz file by checking the magic bytes + file, err := os.Open(expectedFile) + if err == nil { + defer file.Close() + // gzip magic bytes are 0x1f 0x8b + magic := make([]byte, 2) + if n, err := file.Read(magic); err == nil && n == 2 { + if magic[0] != 0x1f || magic[1] != 0x8b { + t.Errorf("Downloaded file is not a valid gzip file (magic bytes: %x)", magic) + } + } + } + + // Verify no lock file was left behind + lockFile := expectedFile + ".lock" + if _, err := os.Stat(lockFile); err == nil { + t.Errorf("Lock file %s was not cleaned up", lockFile) + } + } +} diff --git a/pkg/helm/pkg/downloader/manager.go b/pkg/helm/pkg/downloader/manager.go index 26e08a7c..4a4c0ed0 100644 --- a/pkg/helm/pkg/downloader/manager.go +++ b/pkg/helm/pkg/downloader/manager.go @@ -16,34 +16,34 @@ limitations under the License. package downloader import ( + "context" "crypto" "encoding/hex" + "errors" "fmt" "io" + stdfs "io/fs" "log" "net/url" "os" - "path" "path/filepath" "regexp" "strings" "sync" "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" "sigs.k8s.io/yaml" "github.com/werf/nelm/pkg/helm/intern/resolver" "github.com/werf/nelm/pkg/helm/intern/third_party/dep/fs" "github.com/werf/nelm/pkg/helm/intern/urlutil" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/repo" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" ) // ErrRepoNotFound indicates that chart repositories can't be found in local repo cache. @@ -77,8 +77,14 @@ type Manager struct { RepositoryConfig string RepositoryCache string - // AllowMissingRepos allows usage of dependency build without adding repos AllowMissingRepos bool + + // ContentCache is a location where a cache of charts can be stored + ContentCache string +} + +func (m *Manager) SetChartPath(path string) { + m.ChartPath = path } // Build rebuilds a local charts directory from a lockfile. @@ -86,8 +92,8 @@ type Manager struct { // If the lockfile is not present, this will run a Manager.Update() // // If SkipUpdate is set, this will not update the repository. -func (m *Manager) Build(opts helmopts.HelmOptions) error { - c, err := m.loadChartDir(opts) +func (m *Manager) Build(ctx context.Context) error { + c, err := m.loadChartDir(ctx) if err != nil { return err } @@ -96,7 +102,7 @@ func (m *Manager) Build(opts helmopts.HelmOptions) error { // an update. lock := c.Lock if lock == nil { - return m.Update(opts) + return m.Update(ctx) } // Check that all of the repos we're dependent on actually exist. @@ -145,13 +151,13 @@ func (m *Manager) Build(opts helmopts.HelmOptions) error { if !m.SkipUpdate { // For each repo in the file, update the cached copy of that repo - if err := m.UpdateRepositories(); err != nil { + if err := m.UpdateRepositories(ctx); err != nil { return err } } // Now we need to fetch every package here into charts/ - return m.downloadAll(lock.Dependencies, opts) + return m.downloadAll(ctx, lock.Dependencies) } // Update updates a local charts directory. @@ -159,8 +165,8 @@ func (m *Manager) Build(opts helmopts.HelmOptions) error { // It first reads the Chart.yaml file, and then attempts to // negotiate versions based on that. It will download the versions // from remote chart repositories unless SkipUpdate is true. -func (m *Manager) Update(opts helmopts.HelmOptions) error { - c, err := m.loadChartDir(opts) +func (m *Manager) Update(ctx context.Context) error { + c, err := m.loadChartDir(ctx) if err != nil { return err } @@ -183,7 +189,7 @@ func (m *Manager) Update(opts helmopts.HelmOptions) error { // has some information about them and, when possible, the index files // locally. // TODO(mattfarina): Repositories should be explicitly added by end users - // rather than automattic. In Helm v4 require users to add repositories. They + // rather than automatic. In Helm v4 require users to add repositories. They // should have to add them in order to make sure they are aware of the // repositories and opt-in to any locations, for security. repoNames, err = m.ensureMissingRepos(repoNames, req) @@ -194,20 +200,20 @@ func (m *Manager) Update(opts helmopts.HelmOptions) error { // For each of the repositories Helm is configured to know about, update // the index information locally. if !m.SkipUpdate { - if err := m.UpdateRepositories(); err != nil { + if err := m.UpdateRepositories(ctx); err != nil { return err } } // Now we need to find out which version of a chart best satisfies the // dependencies in the Chart.yaml - lock, err := m.resolve(req, repoNames, opts) + lock, err := m.resolve(ctx, req, repoNames) if err != nil { return err } // Now we need to fetch every package here into charts/ - if err := m.downloadAll(lock.Dependencies, opts); err != nil { + if err := m.downloadAll(ctx, lock.Dependencies); err != nil { return err } @@ -228,42 +234,42 @@ func (m *Manager) Update(opts helmopts.HelmOptions) error { return writeLock(m.ChartPath, lock, c.Metadata.APIVersion == chart.APIVersionV1) } -func (m *Manager) loadChartDir(opts helmopts.HelmOptions) (*chart.Chart, error) { +func (m *Manager) loadChartDir(ctx context.Context) (*chart.Chart, error) { if fi, err := os.Stat(m.ChartPath); err != nil { - return nil, errors.Wrapf(err, "could not find %s", m.ChartPath) + return nil, fmt.Errorf("could not find %s: %w", m.ChartPath, err) } else if !fi.IsDir() { return nil, errors.New("only unpacked charts can be updated") } - return loader.LoadDir(m.ChartPath, opts) + return loader.LoadDir(ctx, m.ChartPath) } // resolve takes a list of dependencies and translates them into an exact version to download. // // This returns a lock file, which has all of the dependencies normalized to a specific version. -func (m *Manager) resolve(req []*chart.Dependency, repoNames map[string]string, opts helmopts.HelmOptions) (*chart.Lock, error) { +func (m *Manager) resolve(ctx context.Context, req []*chart.Dependency, repoNames map[string]string) (*chart.Lock, error) { res := resolver.New(m.ChartPath, m.RepositoryCache, m.RegistryClient) - return res.Resolve(req, repoNames, opts) + return res.Resolve(ctx, req, repoNames) } // downloadAll takes a list of dependencies and downloads them into charts/ // // It will delete versions of the chart that exist on disk and might cause // a conflict. -func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOptions) error { +func (m *Manager) downloadAll(ctx context.Context, deps []*chart.Dependency) error { repos, err := m.loadChartRepositories() if err != nil { return err } destPath := filepath.Join(m.ChartPath, "charts") - tmpPath := filepath.Join(m.ChartPath, "tmpcharts") + tmpPath := filepath.Join(m.ChartPath, fmt.Sprintf("tmpcharts-%d", os.Getpid())) // Check if 'charts' directory is not actually a directory. If it does not exist, create it. if fi, err := os.Stat(destPath); err == nil { if !fi.IsDir() { - return errors.Errorf("%q is not a directory", destPath) + return fmt.Errorf("%q is not a directory", destPath) } - } else if os.IsNotExist(err) { + } else if errors.Is(err, stdfs.ErrNotExist) { if err := os.MkdirAll(destPath, 0755); err != nil { return err } @@ -286,7 +292,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption fmt.Fprintf(m.Out, "Dependency %s did not declare a repository. Assuming it exists in the charts directory\n", dep.Name) // NOTE: we are only validating the local dependency conforms to the constraints. No copying to tmpPath is necessary. chartPath := filepath.Join(destPath, dep.Name) - ch, err := loader.LoadDir(chartPath, opts) + ch, err := loader.LoadDir(ctx, chartPath) if err != nil { return fmt.Errorf("unable to load chart '%s': %v", chartPath, err) } @@ -311,7 +317,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption if m.Debug { fmt.Fprintf(m.Out, "Archiving %s from repo %s\n", dep.Name, dep.Repository) } - ver, err := tarFromLocalDir(m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath, opts) + ver, err := tarFromLocalDir(ctx, m.ChartPath, dep.Name, dep.Repository, dep.Version, tmpPath) if err != nil { saveError = err break @@ -322,9 +328,9 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption // Any failure to resolve/download a chart should fail: // https://github.com/helm/helm/issues/1439 - churl, username, password, insecureskiptlsverify, passcredentialsall, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) + churl, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err := m.findChartURL(dep.Name, dep.Version, dep.Repository, repos) if err != nil { - saveError = errors.Wrapf(err, "could not find %s", churl) + saveError = fmt.Errorf("could not find %s: %w", churl, err) break } @@ -341,12 +347,13 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption Keyring: m.Keyring, RepositoryConfig: m.RepositoryConfig, RepositoryCache: m.RepositoryCache, + ContentCache: m.ContentCache, RegistryClient: m.RegistryClient, Getters: m.Getters, Options: []getter.Option{ getter.WithBasicAuth(username, password), - getter.WithPassCredentialsAll(passcredentialsall), - getter.WithInsecureSkipVerifyTLS(insecureskiptlsverify), + getter.WithPassCredentialsAll(passCredentialsAll), + getter.WithInsecureSkipVerifyTLS(insecureSkipTLSVerify), getter.WithTLSClientConfig(certFile, keyFile, caFile), }, } @@ -355,7 +362,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption if registry.IsOCI(churl) { churl, version, err = parseOCIRef(churl) if err != nil { - return errors.Wrapf(err, "could not parse OCI reference") + return fmt.Errorf("could not parse OCI reference: %w", err) } dl.Options = append(dl.Options, getter.WithRegistryClient(m.RegistryClient), @@ -363,7 +370,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption } if _, _, err = dl.DownloadTo(churl, version, tmpPath); err != nil { - saveError = errors.Wrapf(err, "could not download %s", churl) + saveError = fmt.Errorf("could not download %s: %w", churl, err) break } @@ -373,7 +380,7 @@ func (m *Manager) downloadAll(deps []*chart.Dependency, opts helmopts.HelmOption // TODO: this should probably be refactored to be a []error, so we can capture and provide more information rather than "last error wins". if saveError == nil { // now we can move all downloaded charts to destPath and delete outdated dependencies - if err := m.safeMoveDeps(deps, tmpPath, destPath, opts); err != nil { + if err := m.safeMoveDeps(ctx, deps, tmpPath, destPath); err != nil { return err } } else { @@ -387,7 +394,7 @@ func parseOCIRef(chartRef string) (string, string, error) { refTagRegexp := regexp.MustCompile(`^(oci://[^:]+(:[0-9]{1,5})?[^:]+):(.*)$`) caps := refTagRegexp.FindStringSubmatch(chartRef) if len(caps) != 4 { - return "", "", errors.Errorf("improperly formatted oci chart reference: %s", chartRef) + return "", "", fmt.Errorf("improperly formatted oci chart reference: %s", chartRef) } chartRef = caps[1] tag := caps[3] @@ -395,7 +402,7 @@ func parseOCIRef(chartRef string) (string, string, error) { return chartRef, tag, nil } -// safeMoveDep moves all dependencies in the source and moves them into dest. +// safeMoveDeps moves all dependencies in the source and moves them into dest. // // It does this by first matching the file name to an expected pattern, then loading // the file to verify that it is a chart. @@ -406,7 +413,7 @@ func parseOCIRef(chartRef string) (string, string, error) { // // This will only return errors that should stop processing entirely. Other errors // will emit log messages or be ignored. -func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string, opts helmopts.HelmOptions) error { +func (m *Manager) safeMoveDeps(ctx context.Context, deps []*chart.Dependency, source, dest string) error { existsInSourceDirectory := map[string]bool{} isLocalDependency := map[string]bool{} sourceFiles, err := os.ReadDir(source) @@ -433,7 +440,7 @@ func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string, op sourcefile := filepath.Join(source, filename) destfile := filepath.Join(dest, filename) existsInSourceDirectory[filename] = true - if _, err := loader.LoadFile(sourcefile, opts); err != nil { + if _, err := loader.LoadFile(ctx, sourcefile); err != nil { fmt.Fprintf(m.Out, "Could not verify %s for moving: %s (Skipping)", sourcefile, err) continue } @@ -449,7 +456,7 @@ func (m *Manager) safeMoveDeps(deps []*chart.Dependency, source, dest string, op for _, file := range destFiles { if !file.IsDir() && !existsInSourceDirectory[file.Name()] { fname := filepath.Join(dest, file.Name()) - ch, err := loader.LoadFile(fname, opts) + ch, err := loader.LoadFile(ctx, fname) if err != nil { fmt.Fprintf(m.Out, "Could not verify %s for deletion: %s (Skipping)\n", fname, err) continue @@ -569,7 +576,7 @@ func (m *Manager) ensureMissingRepos(repoNames map[string]string, deps []*chart. func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, error) { rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { - if os.IsNotExist(err) { + if errors.Is(err, stdfs.ErrNotExist) { return make(map[string]string), nil } return nil, err @@ -652,7 +659,7 @@ repository, use "https://charts.example.com/" or "@example" instead of } // UpdateRepositories updates all of the local repos to the latest. -func (m *Manager) UpdateRepositories() error { +func (m *Manager) UpdateRepositories(ctx context.Context) error { rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { return err @@ -669,10 +676,28 @@ func (m *Manager) UpdateRepositories() error { return nil } +// Filter out duplicate repos by URL, including those with trailing slashes. +func dedupeRepos(repos []*repo.Entry) []*repo.Entry { + seen := make(map[string]*repo.Entry) + for _, r := range repos { + // Normalize URL by removing trailing slashes. + seenURL := strings.TrimSuffix(r.URL, "/") + seen[seenURL] = r + } + var unique []*repo.Entry + for _, r := range seen { + unique = append(unique, r) + } + return unique +} + func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { var wg sync.WaitGroup - for _, c := range repos { + + localRepos := dedupeRepos(repos) + + for _, c := range localRepos { r, err := repo.NewChartRepository(c, m.Getters) if err != nil { return err @@ -713,13 +738,12 @@ func (m *Manager) parallelRepoUpdate(repos []*repo.Entry) error { // repoURL is the repository to search // // If it finds a URL that is "relative", it will prepend the repoURL. -func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureskiptlsverify, passcredentialsall bool, caFile, certFile, keyFile string, err error) { +func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*repo.ChartRepository) (url, username, password string, insecureSkipTLSVerify, passCredentialsAll bool, caFile, certFile, keyFile string, err error) { if registry.IsOCI(repoURL) { return fmt.Sprintf("%s/%s:%s", repoURL, name, version), "", "", false, false, "", "", "", nil } for _, cr := range repos { - if urlutil.Equal(repoURL, cr.Config.URL) { var entry repo.ChartVersions entry, err = findEntryByName(name, cr) @@ -736,15 +760,15 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]* //nolint:nakedret return } - url, err = normalizeURL(repoURL, ve.URLs[0]) + url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0]) if err != nil { //nolint:nakedret return } username = cr.Config.Username password = cr.Config.Password - passcredentialsall = cr.Config.PassCredentialsAll - insecureskiptlsverify = cr.Config.InsecureSkipTLSverify + passCredentialsAll = cr.Config.PassCredentialsAll + insecureSkipTLSVerify = cr.Config.InsecureSkipTLSVerify caFile = cr.Config.CAFile certFile = cr.Config.CertFile keyFile = cr.Config.KeyFile @@ -752,11 +776,11 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]* return } } - url, err = repo.FindChartInRepoURL(repoURL, name, version, certFile, keyFile, caFile, m.Getters) + url, err = repo.FindChartInRepoURL(repoURL, name, m.Getters, repo.WithChartVersion(version), repo.WithClientTLS(certFile, keyFile, caFile)) if err == nil { return url, username, password, false, false, "", "", "", err } - err = errors.Errorf("chart %s not found in %s: %s", name, repoURL, err) + err = fmt.Errorf("chart %s not found in %s: %w", name, repoURL, err) return url, username, password, false, false, "", "", "", err } @@ -802,24 +826,6 @@ func versionEquals(v1, v2 string) bool { return sv1.Equal(sv2) } -func normalizeURL(baseURL, urlOrPath string) (string, error) { - u, err := url.Parse(urlOrPath) - if err != nil { - return urlOrPath, err - } - if u.IsAbs() { - return u.String(), nil - } - u2, err := url.Parse(baseURL) - if err != nil { - return urlOrPath, errors.Wrap(err, "base URL failed to parse") - } - - u2.RawPath = path.Join(u2.RawPath, urlOrPath) - u2.Path = path.Join(u2.Path, urlOrPath) - return u2.String(), nil -} - // loadChartRepositories reads the repositories.yaml, and then builds a map of // ChartRepositories. // @@ -830,7 +836,7 @@ func (m *Manager) loadChartRepositories() (map[string]*repo.ChartRepository, err // Load repositories.yaml file rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { - return indices, errors.Wrapf(err, "failed to load %s", m.RepositoryConfig) + return indices, fmt.Errorf("failed to load %s: %w", m.RepositoryConfig, err) } for _, re := range rf.Repositories { @@ -862,13 +868,27 @@ func writeLock(chartpath string, lock *chart.Lock, legacyLockfile bool) error { lockfileName = "requirements.lock" } dest := filepath.Join(chartpath, lockfileName) + + info, err := os.Lstat(dest) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("error getting info for %q: %w", dest, err) + } else if err == nil { + if info.Mode()&os.ModeSymlink != 0 { + link, err := os.Readlink(dest) + if err != nil { + return fmt.Errorf("error reading symlink for %q: %w", dest, err) + } + return fmt.Errorf("the %s file is a symlink to %q", lockfileName, link) + } + } + return os.WriteFile(dest, data, 0644) } // archive a dep chart from local directory and save it into destPath -func tarFromLocalDir(chartpath, name, repo, version, destPath string, opts helmopts.HelmOptions) (string, error) { +func tarFromLocalDir(ctx context.Context, chartpath, name, repo, version, destPath string) (string, error) { if !strings.HasPrefix(repo, "file://") { - return "", errors.Errorf("wrong format: chart %s repository %s", name, repo) + return "", fmt.Errorf("wrong format: chart %s repository %s", name, repo) } origPath, err := resolver.GetLocalPath(repo, chartpath) @@ -876,14 +896,14 @@ func tarFromLocalDir(chartpath, name, repo, version, destPath string, opts helmo return "", err } - ch, err := loader.LoadDir(origPath, opts) + ch, err := loader.LoadDir(ctx, origPath) if err != nil { return "", err } constraint, err := semver.NewConstraint(version) if err != nil { - return "", errors.Wrapf(err, "dependency %s has an invalid version/constraint format", name) + return "", fmt.Errorf("dependency %s has an invalid version/constraint format: %w", name, err) } v, err := semver.NewVersion(ch.Metadata.Version) @@ -896,7 +916,7 @@ func tarFromLocalDir(chartpath, name, repo, version, destPath string, opts helmo return ch.Metadata.Version, err } - return "", errors.Errorf("can't get a valid version for dependency %s", name) + return "", fmt.Errorf("can't get a valid version for dependency %s", name) } // The prefix to use for cache keys created by the manager for repo names @@ -913,9 +933,3 @@ func key(name string) (string, error) { } return hex.EncodeToString(hash.Sum(nil)), nil } - -func (m *Manager) SetChartPath(path string) { - m.ChartPath = path -} - -var _ helmopts.DepDownloader = (*Manager)(nil) diff --git a/pkg/helm/pkg/downloader/manager_test.go b/pkg/helm/pkg/downloader/manager_test.go index a0bd7da3..5ca10582 100644 --- a/pkg/helm/pkg/downloader/manager_test.go +++ b/pkg/helm/pkg/downloader/manager_test.go @@ -17,16 +17,24 @@ package downloader import ( "bytes" + "context" + "errors" + "io/fs" "os" "path/filepath" "reflect" "testing" + "time" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/repo/repotest" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1/repotest" ) func TestVersionEquals(t *testing.T) { @@ -48,26 +56,6 @@ func TestVersionEquals(t *testing.T) { } } -func TestNormalizeURL(t *testing.T) { - tests := []struct { - name, base, path, expect string - }{ - {name: "basic URL", base: "https://example.com", path: "http://helm.sh/foo", expect: "http://helm.sh/foo"}, - {name: "relative path", base: "https://helm.sh/charts", path: "foo", expect: "https://helm.sh/charts/foo"}, - {name: "Encoded path", base: "https://helm.sh/a%2Fb/charts", path: "foo", expect: "https://helm.sh/a%2Fb/charts/foo"}, - } - - for _, tt := range tests { - got, err := normalizeURL(tt.base, tt.path) - if err != nil { - t.Errorf("%s: error %s", tt.name, err) - continue - } else if got != tt.expect { - t.Errorf("%s: expected %q, got %q", tt.name, tt.expect, got) - } - } -} - func TestFindChartURL(t *testing.T) { var b bytes.Buffer m := &Manager{ @@ -129,6 +117,31 @@ func TestFindChartURL(t *testing.T) { if passcredentialsall != false { t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) } + + name = "foo" + version = "1.2.3" + repoURL = "http://example.com/helm" + + churl, username, password, insecureSkipTLSVerify, passcredentialsall, _, _, _, err = m.findChartURL(name, version, repoURL, repos) + if err != nil { + t.Fatal(err) + } + + if churl != "http://example.com/helm/charts/foo-1.2.3.tgz" { + t.Errorf("Unexpected URL %q", churl) + } + if username != "" { + t.Errorf("Unexpected username %q", username) + } + if password != "" { + t.Errorf("Unexpected password %q", password) + } + if passcredentialsall != false { + t.Errorf("Unexpected passcredentialsall %t", passcredentialsall) + } + if insecureSkipTLSVerify { + t.Errorf("Unexpected insecureSkipTLSVerify %t", insecureSkipTLSVerify) + } } func TestGetRepoNames(t *testing.T) { @@ -224,7 +237,7 @@ func TestDownloadAll(t *testing.T) { RepositoryCache: repoCache, ChartPath: chartPath, } - signtest, err := loader.LoadDir(filepath.Join("testdata", "signtest")) + signtest, err := loader.LoadDir(context.Background(), filepath.Join("testdata", "signtest")) if err != nil { t.Fatal(err) } @@ -232,7 +245,7 @@ func TestDownloadAll(t *testing.T) { t.Fatal(err) } - local, err := loader.LoadDir(filepath.Join("testdata", "local-subchart")) + local, err := loader.LoadDir(context.Background(), filepath.Join("testdata", "local-subchart")) if err != nil { t.Fatal(err) } @@ -255,11 +268,11 @@ func TestDownloadAll(t *testing.T) { if err := os.MkdirAll(filepath.Join(chartPath, "tmpcharts"), 0755); err != nil { t.Fatal(err) } - if err := m.downloadAll([]*chart.Dependency{signDep, localDep}); err != nil { + if err := m.downloadAll(context.Background(), []*chart.Dependency{signDep, localDep}); err != nil { t.Error(err) } - if _, err := os.Stat(filepath.Join(chartPath, "charts", "signtest-0.1.0.tgz")); os.IsNotExist(err) { + if _, err := os.Stat(filepath.Join(chartPath, "charts", "signtest-0.1.0.tgz")); errors.Is(err, fs.ErrNotExist) { t.Error(err) } @@ -284,7 +297,7 @@ version: 0.1.0` Version: "0.1.0", } - err = m.downloadAll([]*chart.Dependency{badLocalDep}) + err = m.downloadAll(context.Background(), []*chart.Dependency{badLocalDep}) if err == nil { t.Fatal("Expected error for bad dependency name") } @@ -292,10 +305,10 @@ version: 0.1.0` func TestUpdateBeforeBuild(t *testing.T) { // Set up a fake repo - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + ) defer srv.Stop() if err := srv.LinkIndices(); err != nil { t.Fatal(err) @@ -347,13 +360,11 @@ func TestUpdateBeforeBuild(t *testing.T) { } // Update before Build. see issue: https://github.com/helm/helm/issues/7101 - err = m.Update() - if err != nil { + if err := m.Update(context.Background()); err != nil { t.Fatal(err) } - err = m.Build() - if err != nil { + if err := m.Build(context.Background()); err != nil { t.Fatal(err) } } @@ -363,10 +374,10 @@ func TestUpdateBeforeBuild(t *testing.T) { // to be fetched. func TestUpdateWithNoRepo(t *testing.T) { // Set up a fake repo - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + ) defer srv.Stop() if err := srv.LinkIndices(); err != nil { t.Fatal(err) @@ -422,8 +433,7 @@ func TestUpdateWithNoRepo(t *testing.T) { } // Test the update - err = m.Update() - if err != nil { + if err := m.Update(context.Background()); err != nil { t.Fatal(err) } } @@ -435,11 +445,12 @@ func TestUpdateWithNoRepo(t *testing.T) { // Parent chart includes local-subchart 0.1.0 subchart from a fake repository, by default. // If each of these main fields (name, version, repository) is not supplied by dep param, default value will be used. func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Dependency) { + t.Helper() // Set up a fake repo - srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*") - if err != nil { - t.Fatal(err) - } + srv := repotest.NewTempServer( + t, + repotest.WithChartSourceGlob("testdata/*.tgz*"), + ) defer srv.Stop() if err := srv.LinkIndices(); err != nil { t.Fatal(err) @@ -478,23 +489,23 @@ func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Depe Schemes: []string{"http", "https"}, New: getter.NewHTTPGetter, }} + contentCache := t.TempDir() m := &Manager{ ChartPath: dir(chartName), Out: b, Getters: g, RepositoryConfig: dir("repositories.yaml"), RepositoryCache: dir(), + ContentCache: contentCache, } // First build will update dependencies and create Chart.lock file. - err = m.Build() - if err != nil { + if err := m.Build(context.Background()); err != nil { t.Fatal(err) } // Second build should be passed. See PR #6655. - err = m.Build() - if err != nil { + if err := m.Build(context.Background()); err != nil { t.Fatal(err) } } @@ -598,3 +609,162 @@ func TestKey(t *testing.T) { } } } + +// Test dedupeRepos tests that the dedupeRepos function correctly deduplicates +func TestDedupeRepos(t *testing.T) { + tests := []struct { + name string + repos []*repo.Entry + want []*repo.Entry + }{ + { + name: "no duplicates", + repos: []*repo.Entry{ + { + URL: "https://example.com/charts", + }, + { + URL: "https://example.com/charts2", + }, + }, + want: []*repo.Entry{ + { + URL: "https://example.com/charts", + }, + { + URL: "https://example.com/charts2", + }, + }, + }, + { + name: "duplicates", + repos: []*repo.Entry{ + { + URL: "https://example.com/charts", + }, + { + URL: "https://example.com/charts", + }, + }, + want: []*repo.Entry{ + { + URL: "https://example.com/charts", + }, + }, + }, + { + name: "duplicates with trailing slash", + repos: []*repo.Entry{ + { + URL: "https://example.com/charts", + }, + { + URL: "https://example.com/charts/", + }, + }, + want: []*repo.Entry{ + { + // the last one wins + URL: "https://example.com/charts/", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := dedupeRepos(tt.repos) + assert.ElementsMatch(t, tt.want, got) + }) + } +} + +func TestWriteLock(t *testing.T) { + fixedTime, err := time.Parse(time.RFC3339, "2025-07-04T00:00:00Z") + assert.NoError(t, err) + lock := &chart.Lock{ + Generated: fixedTime, + Digest: "sha256:12345", + Dependencies: []*chart.Dependency{ + { + Name: "fantastic-chart", + Version: "1.2.3", + Repository: "https://example.com/charts", + }, + }, + } + expectedContent, err := yaml.Marshal(lock) + assert.NoError(t, err) + + t.Run("v2 lock file", func(t *testing.T) { + dir := t.TempDir() + err := writeLock(dir, lock, false) + assert.NoError(t, err) + + lockfilePath := filepath.Join(dir, "Chart.lock") + _, err = os.Stat(lockfilePath) + assert.NoError(t, err, "Chart.lock should exist") + + content, err := os.ReadFile(lockfilePath) + assert.NoError(t, err) + assert.Equal(t, expectedContent, content) + + // Check that requirements.lock does not exist + _, err = os.Stat(filepath.Join(dir, "requirements.lock")) + assert.Error(t, err) + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("v1 lock file", func(t *testing.T) { + dir := t.TempDir() + err := writeLock(dir, lock, true) + assert.NoError(t, err) + + lockfilePath := filepath.Join(dir, "requirements.lock") + _, err = os.Stat(lockfilePath) + assert.NoError(t, err, "requirements.lock should exist") + + content, err := os.ReadFile(lockfilePath) + assert.NoError(t, err) + assert.Equal(t, expectedContent, content) + + // Check that Chart.lock does not exist + _, err = os.Stat(filepath.Join(dir, "Chart.lock")) + assert.Error(t, err) + assert.True(t, os.IsNotExist(err)) + }) + + t.Run("overwrite existing lock file", func(t *testing.T) { + dir := t.TempDir() + lockfilePath := filepath.Join(dir, "Chart.lock") + assert.NoError(t, os.WriteFile(lockfilePath, []byte("old content"), 0644)) + + err = writeLock(dir, lock, false) + assert.NoError(t, err) + + content, err := os.ReadFile(lockfilePath) + assert.NoError(t, err) + assert.Equal(t, expectedContent, content) + }) + + t.Run("lock file is a symlink", func(t *testing.T) { + dir := t.TempDir() + dummyFile := filepath.Join(dir, "dummy.txt") + assert.NoError(t, os.WriteFile(dummyFile, []byte("dummy"), 0644)) + + lockfilePath := filepath.Join(dir, "Chart.lock") + assert.NoError(t, os.Symlink(dummyFile, lockfilePath)) + + err = writeLock(dir, lock, false) + assert.Error(t, err) + assert.Contains(t, err.Error(), "the Chart.lock file is a symlink to") + }) + + t.Run("chart path is not a directory", func(t *testing.T) { + dir := t.TempDir() + filePath := filepath.Join(dir, "not-a-dir") + assert.NoError(t, os.WriteFile(filePath, []byte("file"), 0644)) + + err = writeLock(filePath, lock, false) + assert.Error(t, err) + }) +} diff --git a/pkg/helm/pkg/downloader/testdata/repository/testing-relative-index.yaml b/pkg/helm/pkg/downloader/testdata/repository/testing-relative-index.yaml index ba27ed25..9524daf6 100644 --- a/pkg/helm/pkg/downloader/testdata/repository/testing-relative-index.yaml +++ b/pkg/helm/pkg/downloader/testdata/repository/testing-relative-index.yaml @@ -26,3 +26,16 @@ entries: version: 1.2.3 checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d apiVersion: v2 + baz: + - name: baz + description: Baz Chart With Absolute Path + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - /path/to/baz-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d + apiVersion: v2 diff --git a/pkg/helm/pkg/engine/doc.go b/pkg/helm/pkg/engine/doc.go index 6b3443aa..c70f3f34 100644 --- a/pkg/helm/pkg/engine/doc.go +++ b/pkg/helm/pkg/engine/doc.go @@ -21,4 +21,4 @@ When Helm renders templates it does so with additional functions and different modes (e.g., strict, lint mode). This package handles the helm specific implementation. */ -package engine // import "helm.sh/helm/v3/pkg/engine" +package engine // import "github.com/werf/nelm/pkg/helm/pkg/engine" diff --git a/pkg/helm/pkg/engine/engine.go b/pkg/helm/pkg/engine/engine.go index e12eeed0..6824baa6 100644 --- a/pkg/helm/pkg/engine/engine.go +++ b/pkg/helm/pkg/engine/engine.go @@ -17,8 +17,12 @@ limitations under the License. package engine import ( + "context" + "errors" "fmt" "log" + "log/slog" + "maps" "path" "path/filepath" "regexp" @@ -28,18 +32,20 @@ import ( "unicode" "github.com/davecgh/go-spew/spew" - "github.com/pkg/errors" - "github.com/samber/lo" + "github.com/werf/common-go/pkg/util" "k8s.io/client-go/rest" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/gotmplfunctions" - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/runtimedata" - "github.com/werf/common-go/pkg/util" + nelmcommon "github.com/werf/nelm/pkg/common" + v3 "github.com/werf/nelm/pkg/helm/intern/chart/v3" + ci "github.com/werf/nelm/pkg/helm/pkg/chart" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + v2 "github.com/werf/nelm/pkg/helm/pkg/chart/v2" ) +type secretFilesRuntimeData interface { + GetDecryptedSecretFilesData() map[string]string +} + // Engine is an implementation of the Helm rendering implementation for templates. type Engine struct { // If strict is enabled, template rendering will fail if a template references @@ -51,6 +57,9 @@ type Engine struct { clientProvider *ClientProvider // EnableDNS tells the engine to allow DNS lookups when rendering templates EnableDNS bool + // CustomTemplateFuncs is defined by users to provide custom template funcs + CustomTemplateFuncs template.FuncMap + secretsRuntimeData chartcommon.RuntimeData } // New creates a new instance of Engine using the passed in rest config. @@ -80,35 +89,36 @@ func New(config *rest.Config) Engine { // that section of the values will be passed into the "foo" chart. And if that // section contains a value named "bar", that value will be passed on to the // bar chart during render time. -func (e Engine) Render(chrt *chart.Chart, values chartutil.Values, opts helmopts.HelmOptions) (map[string]string, error) { - tmap := allTemplates(chrt, values, opts) - return e.render(tmap, chrt.SecretsRuntimeData, opts) +func (e Engine) Render(ctx context.Context, chrt ci.Charter, values chartcommon.Values) (map[string]string, error) { + e.secretsRuntimeData = secretsRuntimeDataFromChart(chrt) + tmap := allTemplates(chrt, values) + return e.render(ctx, tmap) } // Render takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. -func Render(chrt *chart.Chart, values chartutil.Values, opts helmopts.HelmOptions) (map[string]string, error) { - return new(Engine).Render(chrt, values, opts) +func Render(ctx context.Context, chrt ci.Charter, values chartcommon.Values) (map[string]string, error) { + return new(Engine).Render(ctx, chrt, values) } // RenderWithClient takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. This engine is client aware and so can have template // functions that interact with the client. -func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config, opts helmopts.HelmOptions) (map[string]string, error) { +func RenderWithClient(ctx context.Context, chrt ci.Charter, values chartcommon.Values, config *rest.Config) (map[string]string, error) { var clientProvider ClientProvider = clientProviderFromConfig{config} return Engine{ clientProvider: &clientProvider, - }.Render(chrt, values, opts) + }.Render(ctx, chrt, values) } // RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to // render the Go templates using the default options. This engine is client aware and so can have template // functions that interact with the client. // This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed. -func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider, opts helmopts.HelmOptions) (map[string]string, error) { +func RenderWithClientProvider(ctx context.Context, chrt ci.Charter, values chartcommon.Values, clientProvider ClientProvider) (map[string]string, error) { return Engine{ clientProvider: &clientProvider, - }.Render(chrt, values, opts) + }.Render(ctx, chrt, values) } // renderable is an object that can be rendered. @@ -116,7 +126,7 @@ type renderable struct { // tpl is the current template. tpl string // vals are the values to be supplied to the template. - vals chartutil.Values + vals chartcommon.Values // namespace prefix to the templates of the current chart basePath string } @@ -127,6 +137,8 @@ const recursionMaxNums = 1000 var warnRegex = regexp.MustCompile(warnStartDelim + `((?s).*)` + warnEndDelim) +var Debug bool + func warnWrap(warn string) string { return warnStartDelim + warn + warnEndDelim } @@ -138,7 +150,9 @@ func includeFun(t *template.Template, includedNames map[string]int) func(string, var buf strings.Builder if v, ok := includedNames[name]; ok { if v > recursionMaxNums { - return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name) + return "", fmt.Errorf( + "rendering template has a nested reference name: %s: %w", + name, errors.New("unable to execute template")) } includedNames[name]++ } else { @@ -152,7 +166,6 @@ func includeFun(t *template.Template, includedNames map[string]int) func(string, templateName: name, }, Debug, err) } - return buf.String(), nil } } @@ -161,14 +174,9 @@ func includeFun(t *template.Template, includedNames map[string]int) func(string, // defined by their enclosing contexts. func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) { return func(tpl string, vals interface{}) (string, error) { - // No templating required if plain text with no templates passed. - if !strings.Contains(tpl, "{{") && !strings.Contains(tpl, "}}") { - return tpl, nil - } - t, err := parent.Clone() if err != nil { - return "", errors.Wrapf(err, "cannot clone template") + return "", fmt.Errorf("cannot clone template: %w", err) } // Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022 @@ -183,15 +191,14 @@ func tplFun(parent *template.Template, includedNames map[string]int, strict bool // Re-inject 'include' so that it can close over our clone of t; // this lets any 'define's inside tpl be 'include'd. t.Funcs(template.FuncMap{ - "include": includeFun(t, includedNames), - "tpl": tplFun(t, includedNames, strict), - + "include": includeFun(t, includedNames), + "tpl": tplFun(t, includedNames, strict), "include_debug": includeDebugFun(t, includedNames), "tpl_debug": tplDebugFun(t, includedNames, strict), }) // We need a .New template, as template text which is just blanks - // or comments after parsing out defines just addes new named + // or comments after parsing out defines just adds new named // template definitions without changing the main template. // https://pkg.go.dev/text/template#Template.Parse // Use the parent's name for lack of a better way to identify the tpl @@ -220,34 +227,63 @@ func tplFun(parent *template.Template, includedNames map[string]int, strict bool } // initFunMap creates the Engine's FuncMap and adds context-specific functions. -func (e Engine) initFunMap(t *template.Template, secretsRuntimeData runtimedata.RuntimeData, opts helmopts.HelmOptions) { +func (e Engine) initFunMap(ctx context.Context, t *template.Template) { funcMap := funcMap() includedNames := make(map[string]int) // Add the template-rendering functions here so we can close over t. - funcMap["include"] = includeFun(t, includedNames) - funcMap["tpl"] = tplFun(t, includedNames, e.Strict) - + include := includeFun(t, includedNames) + tpl := tplFun(t, includedNames, e.Strict) + funcMap["include"] = include + funcMap["tpl"] = tpl funcMap["include_debug"] = includeDebugFun(t, includedNames) funcMap["tpl_debug"] = tplDebugFun(t, includedNames, e.Strict) + funcMap["dump_debug"] = func(value interface{}) string { + if Debug { + log.Printf("-- dump_debug result:\n%s\n\n", spew.Sdump(value)) + } + + return "" + } + funcMap["printf_debug"] = func(format string, args ...interface{}) string { + if Debug { + log.Printf("-- printf_debug format %q result:\n%s\n\n", format, fmt.Sprintf(format, args...)) + } + + return "" + } + switch nelmcommon.HelmOptionsFromContext(ctx).ChartLoadOpts.ChartType { + case nelmcommon.LegacyChartTypeSubchart: + funcMap["werf_secret_file"] = func(_ string) (string, error) { + return "", errors.New("werf_secret_file is not available for subcharts") + } + default: + if e.secretsRuntimeData != nil { + setupWerfSecretFile(e.secretsRuntimeData, funcMap) + } else { + funcMap["werf_secret_file"] = func(_ string) (string, error) { + return "", errors.New("werf_secret_file is not available for this chart type") + } + } + } // Add the `required` function here so we can use lintMode funcMap["required"] = func(warn string, val interface{}) (interface{}, error) { if val == nil { if e.LintMode { // Don't fail on missing required values when linting - log.Printf("[INFO] Missing required value: %s", warn) + slog.Warn("missing required value", "message", warn) return "", nil } - return val, errors.Errorf(warnWrap(warn)) + return val, errors.New(warnWrap(warn)) } else if _, ok := val.(string); ok { if val == "" { if e.LintMode { // Don't fail on missing required values when linting - log.Printf("[INFO] Missing required value: %s", warn) + slog.Warn("missing required values", "message", warn) return "", nil } - return val, errors.Errorf(warnWrap(warn)) + return val, errors.New(warnWrap(warn)) } } return val, nil @@ -257,7 +293,7 @@ func (e Engine) initFunMap(t *template.Template, secretsRuntimeData runtimedata. funcMap["fail"] = func(msg string) (string, error) { if e.LintMode { // Don't fail when linting - log.Printf("[INFO] Fail: %s", msg) + slog.Info("funcMap fail", "message", msg) return "", nil } return "", errors.New(warnWrap(msg)) @@ -272,40 +308,19 @@ func (e Engine) initFunMap(t *template.Template, secretsRuntimeData runtimedata. // When DNS lookups are not enabled override the sprig function and return // an empty string. if !e.EnableDNS { - funcMap["getHostByName"] = func(name string) string { + funcMap["getHostByName"] = func(_ string) string { return "" } } - funcMap["printf_debug"] = func(format string, args ...interface{}) string { - if Debug { - log.Printf("-- printf_debug format %q result:\n%s\n\n", format, fmt.Sprintf(format, args...)) - } - - return "" - } - - funcMap["dump_debug"] = func(obj interface{}) string { - if Debug { - log.Printf("-- dump_debug result:\n%s\n\n", spew.Sdump(obj)) - } - - return "" - } - - switch opts.ChartLoadOpts.ChartType { - case helmopts.ChartTypeBundle, helmopts.ChartTypeChart, helmopts.ChartTypeChartStub: - gotmplfunctions.SetupWerfSecretFile(secretsRuntimeData, funcMap) - case helmopts.ChartTypeSubchart: - default: - panic("unknown extender type") - } + // Set custom template funcs + maps.Copy(funcMap, e.CustomTemplateFuncs) t.Funcs(funcMap) } // render takes a map of templates/values and renders them. -func (e Engine) render(tpls map[string]renderable, secretsRuntimeData runtimedata.RuntimeData, opts helmopts.HelmOptions) (rendered map[string]string, err error) { +func (e Engine) render(ctx context.Context, tpls map[string]renderable) (rendered map[string]string, err error) { // Basically, what we do here is start with an empty parent template and then // build up a list of templates -- one for each file. Once all of the templates // have been parsed, we loop through again and execute every template. @@ -315,7 +330,7 @@ func (e Engine) render(tpls map[string]renderable, secretsRuntimeData runtimedat // template engine. defer func() { if r := recover(); r != nil { - err = errors.Errorf("rendering template failed: %v", r) + err = fmt.Errorf("rendering template failed: %v", r) } }() t := template.New("gotpl") @@ -327,7 +342,7 @@ func (e Engine) render(tpls map[string]renderable, secretsRuntimeData runtimedat t.Option("missingkey=zero") } - e.initFunMap(t, secretsRuntimeData, opts) + e.initFunMap(ctx, t) // We want to parse the templates in a predictable order. The order favors // higher-level (in file system) templates over deeply nested templates. @@ -352,13 +367,13 @@ func (e Engine) render(tpls map[string]renderable, secretsRuntimeData runtimedat } // At render time, add information about the template that is being rendered. vals := tpls[filename].vals - vals["Template"] = chartutil.Values{"Name": filename, "BasePath": tpls[filename].basePath} + vals["Template"] = chartcommon.Values{"Name": filename, "BasePath": tpls[filename].basePath} var buf strings.Builder if err := t.ExecuteTemplate(&buf, filename, vals); err != nil { return map[string]string{}, detailedTemplateError(t, detailedTemplateErrorData{ templateName: filename, templateContent: tpls[filename].tpl, - }, Debug, cleanupExecError(filename, err)) + }, Debug, reformatExecErrorMsg(filename, err)) } // Work around the issue where Go will emit "" even if Options(missing=zero) @@ -381,129 +396,192 @@ func cleanupParseError(filename string, err error) error { location := tokens[1] // The remaining tokens make up a stacktrace-like chain, ending with the relevant error errMsg := tokens[len(tokens)-1] - return fmt.Errorf("parse error at (%s): %s", string(location), errMsg) + return fmt.Errorf("parse error at (%s): %s", location, errMsg) } -func cleanupExecError(filename string, err error) error { - if _, isExecError := err.(template.ExecError); !isExecError { - return err +type TraceableError struct { + location string + message string + executedFunction string +} + +func (t TraceableError) String() string { + var errorString strings.Builder + if t.location != "" { + _, _ = fmt.Fprintf(&errorString, "%s\n ", t.location) + } + if t.executedFunction != "" { + _, _ = fmt.Fprintf(&errorString, "%s\n ", t.executedFunction) } + if t.message != "" { + _, _ = fmt.Fprintf(&errorString, "%s\n", t.message) + } + return errorString.String() +} - tokens := strings.SplitN(err.Error(), ": ", 3) - if len(tokens) != 3 { - // This might happen if a non-templating error occurs - return fmt.Errorf("execution error in (%s): %s", filename, err) +// parseTemplateExecErrorString parses a template execution error string from text/template +// without using regular expressions. It returns a TraceableError and true if parsing succeeded. +func parseTemplateExecErrorString(s string) (TraceableError, bool) { + const prefix = "template: " + if !strings.HasPrefix(s, prefix) { + return TraceableError{}, false } + remainder := s[len(prefix):] - // The first token is "template" - // The second token is either "filename:lineno" or "filename:lineNo:columnNo" - location := tokens[1] + // Special case: "template: no template %q associated with template %q" + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 + traceableError, done := parseTemplateNoTemplateError(s, remainder) + if done { + return traceableError, true + } - parts := warnRegex.FindStringSubmatch(tokens[2]) - if len(parts) >= 2 { - return fmt.Errorf("execution error at (%s): %s", string(location), parts[1]) + // Executing form: ": executing \"\" at <>: [ template:...]" + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 + traceableError, done = parseTemplateExecutingAtErrorType(remainder) + if done { + return traceableError, true } - return err + // Simple form: ": " + // Use LastIndex to avoid splitting colons within line:col info. + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 + traceableError, done = parseTemplateSimpleErrorString(remainder) + if done { + return traceableError, true + } + + return TraceableError{}, false } -func sortTemplates(tpls map[string]renderable) []string { - keys := make([]string, len(tpls)) - i := 0 - for key := range tpls { - keys[i] = key - i++ +// Special case: "template: no template %q associated with template %q" +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 +func parseTemplateNoTemplateError(s string, remainder string) (TraceableError, bool) { + if strings.HasPrefix(remainder, "no template ") { + return TraceableError{message: s}, true } - sort.Sort(sort.Reverse(byPathLen(keys))) - return keys + return TraceableError{}, false } -type byPathLen []string - -func (p byPathLen) Len() int { return len(p) } -func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] } -func (p byPathLen) Less(i, j int) bool { - a, b := p[i], p[j] - ca, cb := strings.Count(a, "/"), strings.Count(b, "/") - if ca == cb { - return strings.Compare(a, b) == -1 +// Simple form: ": " +// Use LastIndex to avoid splitting colons within line:col info. +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 +func parseTemplateSimpleErrorString(remainder string) (TraceableError, bool) { + if sep := strings.LastIndex(remainder, ": "); sep != -1 { + templateName := remainder[:sep] + errMsg := remainder[sep+2:] + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{location: templateName, message: errMsg}, true } - return ca < cb + return TraceableError{}, false } -// allTemplates returns all templates for a chart and its dependencies. -// -// As it goes, it also prepares the values in a scope-sensitive manner. -func allTemplates(c *chart.Chart, vals chartutil.Values, opts helmopts.HelmOptions) map[string]renderable { - templates := make(map[string]renderable) - recAllTpls(c, templates, vals, opts) - return templates +// Executing form: ": executing \"\" at <>: [ template:...]" +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 +func parseTemplateExecutingAtErrorType(remainder string) (TraceableError, bool) { + if idx := strings.Index(remainder, ": executing "); idx != -1 { + templateName := remainder[:idx] + after := remainder[idx+len(": executing "):] + if len(after) == 0 || after[0] != '"' { + return TraceableError{}, false + } + // find closing quote for function name + endQuote := strings.IndexByte(after[1:], '"') + if endQuote == -1 { + return TraceableError{}, false + } + endQuote++ // account for offset we started at 1 + functionName := after[1:endQuote] + afterFunc := after[endQuote+1:] + + // expect: " at <" then location then ">: " then message + const atPrefix = " at <" + if !strings.HasPrefix(afterFunc, atPrefix) { + return TraceableError{}, false + } + afterAt := afterFunc[len(atPrefix):] + endLoc := strings.Index(afterAt, ">: ") + if endLoc == -1 { + return TraceableError{}, false + } + locationName := afterAt[:endLoc] + errMsg := afterAt[endLoc+len(">: "):] + + // trim chained next error starting with space + "template:" if present + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{ + location: templateName, + message: errMsg, + executedFunction: "executing \"" + functionName + "\" at <" + locationName + ">:", + }, true + } + return TraceableError{}, false } -// recAllTpls recurses through the templates in a chart. -// -// As it recurses, it also sets the values to be appropriate for the template -// scope. -func recAllTpls(c *chart.Chart, templates map[string]renderable, vals chartutil.Values, opts helmopts.HelmOptions) map[string]interface{} { - subCharts := make(map[string]interface{}) - chartMetaData := struct { - chart.Metadata - IsRoot bool - }{*c.Metadata, c.IsRoot()} +// reformatExecErrorMsg takes an error message for template rendering and formats it into a formatted +// multi-line error string +func reformatExecErrorMsg(filename string, err error) error { + // This function parses the error message produced by text/template package. + // If it can parse out details from that error message such as the line number, template it failed on, + // and error description, then it will construct a new error that displays these details in a structured way. + // If there are issues with parsing the error message, the err passed into the function should return instead. + var execError template.ExecError + if !errors.As(err, &execError) { + return err + } - next := map[string]interface{}{ - "Chart": chartMetaData, - "Files": newFiles(c.Files), - "Release": vals["Release"], - "Capabilities": vals["Capabilities"], - "Values": make(chartutil.Values), - "Subcharts": subCharts, - "Runtime": vals["Runtime"], + tokens := strings.SplitN(err.Error(), ": ", 3) + if len(tokens) != 3 { + // This might happen if a non-templating error occurs + return fmt.Errorf("execution error in (%s): %s", filename, err) } - next = lo.Assign(opts.ChartLoadOpts.DefaultRootContext, next) + // The first token is "template" + // The second token is either "filename:lineno" or "filename:lineNo:columnNo" + location := tokens[1] - // If there is a {{.Values.ThisChart}} in the parent metadata, - // copy that into the {{.Values}} for this template. - if c.IsRoot() { - next["Values"] = vals["Values"] - } else if vs, err := vals.Table("Values." + c.Name()); err == nil { - next["Values"] = vs + parts := warnRegex.FindStringSubmatch(tokens[2]) + if len(parts) >= 2 { + return fmt.Errorf("execution error at (%s): %s", location, parts[1]) } - - for _, child := range c.Dependencies() { - subCharts[child.Name()] = recAllTpls(child, templates, next, opts) + current := err + var fileLocations []TraceableError + for current != nil { + if tr, ok := parseTemplateExecErrorString(current.Error()); ok { + if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != tr { + fileLocations = append(fileLocations, tr) + } + } else { + return err + } + current = errors.Unwrap(current) } - newParentID := c.ChartFullPath() - for _, t := range c.Templates { - if t == nil { - continue - } - if !isTemplateValid(c, t.Name) { - continue - } - templates[path.Join(newParentID, t.Name)] = renderable{ - tpl: string(t.Data), - vals: next, - basePath: path.Join(newParentID, "templates"), - } + var finalErrorString strings.Builder + for _, fileLocation := range fileLocations { + _, _ = fmt.Fprintf(&finalErrorString, "%s", fileLocation.String()) } - return next + return errors.New(strings.TrimSpace(finalErrorString.String())) } -// isTemplateValid returns true if the template is valid for the chart type -func isTemplateValid(ch *chart.Chart, templateName string) bool { - if isLibraryChart(ch) { - return strings.HasPrefix(filepath.Base(templateName), "_") - } - return true +var TemplateErrHint = `Set log level to "debug" to get more details about this error.` + +type detailedTemplateErrorData struct { + funcName string + templateName string + templateContent string } -// isLibraryChart returns true if the chart is a library chart -func isLibraryChart(c *chart.Chart) bool { - return strings.EqualFold(c.Metadata.Type, "library") +func templateContentFromTree(tmpl *template.Template, name string) (string, error) { + t := tmpl.Lookup(name) + if t == nil || t.Tree == nil || t.Tree.Root == nil { + return "", fmt.Errorf("template %q not found", name) + } + return strings.TrimSpace(t.Tree.Root.String()), nil } func detailedTemplateError(tmpl *template.Template, d detailedTemplateErrorData, debug bool, err error) error { @@ -511,12 +589,10 @@ func detailedTemplateError(tmpl *template.Template, d detailedTemplateErrorData, if d.templateContent == "" { d.templateContent, _ = templateContentFromTree(tmpl, d.templateName) } - var funcNameMsg string if d.funcName != "" { funcNameMsg = fmt.Sprintf(" Function name: %q\n", d.funcName) } - return fmt.Errorf( "%w\n\nDetails:\n%s Template name: %q\n Template content:\n%s", err, @@ -525,35 +601,23 @@ func detailedTemplateError(tmpl *template.Template, d detailedTemplateErrorData, strings.TrimRightFunc(util.NumerateLines(d.templateContent, 1), unicode.IsSpace), ) } - + if d.funcName == "" || d.funcName == "include" || d.funcName == "tpl" { + return err + } if strings.Contains(err.Error(), TemplateErrHint) { return err } - return fmt.Errorf("%w\n%s", err, TemplateErrHint) } -type detailedTemplateErrorData struct { - funcName string - templateName string - templateContent string -} - -func templateContentFromTree(tmpl *template.Template, name string) (string, error) { - t := tmpl.Lookup(name) - if t == nil || t.Tree == nil || t.Tree.Root == nil { - return "", fmt.Errorf("template %q not found", name) - } - - return strings.TrimSpace(t.Tree.Root.String()), nil -} - func includeDebugFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) { return func(name string, data interface{}) (string, error) { var buf strings.Builder if v, ok := includedNames[name]; ok { if v > recursionMaxNums { - return "", errors.Wrapf(fmt.Errorf("unable to execute template"), "rendering template has a nested reference name: %s", name) + return "", fmt.Errorf( + "rendering template has a nested reference name: %s: %w", + name, errors.New("unable to execute template")) } includedNames[name]++ } else { @@ -589,41 +653,28 @@ func includeDebugFun(t *template.Template, includedNames map[string]int) func(st func tplDebugFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) { return func(tpl string, vals interface{}) (string, error) { - // No templating required if plain text with no templates passed. if !strings.Contains(tpl, "{{") && !strings.Contains(tpl, "}}") { return tpl, nil } t, err := parent.Clone() if err != nil { - return "", errors.Wrapf(err, "cannot clone template") + return "", fmt.Errorf("cannot clone template: %w", err) } - // Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022 - // We have to go by strict from our engine configuration, as the option fields are private in Template. - // TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix. if strict { t.Option("missingkey=error") } else { t.Option("missingkey=zero") } - // Re-inject 'include' so that it can close over our clone of t; - // this lets any 'define's inside tpl be 'include'd. t.Funcs(template.FuncMap{ - "include": includeFun(t, includedNames), - "tpl": tplFun(t, includedNames, strict), - + "include": includeFun(t, includedNames), + "tpl": tplFun(t, includedNames, strict), "include_debug": includeDebugFun(t, includedNames), "tpl_debug": tplDebugFun(t, includedNames, strict), }) - // We need a .New template, as template text which is just blanks - // or comments after parsing out defines just addes new named - // template definitions without changing the main template. - // https://pkg.go.dev/text/template#Template.Parse - // Use the parent's name for lack of a better way to identify the tpl - // text string. (Maybe we could use a hash appended to the name?) t, err = t.New(parent.Name()).Parse(tpl) if err != nil { return "", detailedTemplateError(t, detailedTemplateErrorData{ @@ -646,7 +697,6 @@ func tplDebugFun(parent *template.Template, includedNames map[string]int, strict }, Debug, err) } - // See comment in renderWithReferences explaining the hack. result := strings.ReplaceAll(buf.String(), "", "") if Debug { @@ -657,7 +707,130 @@ func tplDebugFun(parent *template.Template, includedNames map[string]int, strict } } -var ( - TemplateErrHint = `Set log level to "debug" to get more details about this error.` - Debug bool -) +func sortTemplates(tpls map[string]renderable) []string { + keys := make([]string, len(tpls)) + i := 0 + for key := range tpls { + keys[i] = key + i++ + } + sort.Sort(sort.Reverse(byPathLen(keys))) + return keys +} + +type byPathLen []string + +func (p byPathLen) Len() int { return len(p) } +func (p byPathLen) Swap(i, j int) { p[j], p[i] = p[i], p[j] } +func (p byPathLen) Less(i, j int) bool { + a, b := p[i], p[j] + ca, cb := strings.Count(a, "/"), strings.Count(b, "/") + if ca == cb { + return strings.Compare(a, b) == -1 + } + return ca < cb +} + +// allTemplates returns all templates for a chart and its dependencies. +// +// As it goes, it also prepares the values in a scope-sensitive manner. +func allTemplates(c ci.Charter, vals chartcommon.Values) map[string]renderable { + templates := make(map[string]renderable) + recAllTpls(c, templates, vals) + return templates +} + +// recAllTpls recurses through the templates in a chart. +// +// As it recurses, it also sets the values to be appropriate for the template +// scope. +func recAllTpls(c ci.Charter, templates map[string]renderable, values chartcommon.Values) map[string]interface{} { + vals := values.AsMap() + subCharts := make(map[string]interface{}) + accessor, err := ci.NewAccessor(c) + if err != nil { + slog.Error("error accessing chart", "error", err) + } + chartMetaData := accessor.MetadataAsMap() + chartMetaData["IsRoot"] = accessor.IsRoot() + + next := map[string]interface{}{ + "Chart": chartMetaData, + "Files": newFiles(accessor.Files()), + "Release": vals["Release"], + "Capabilities": vals["Capabilities"], + "Values": make(chartcommon.Values), + "Subcharts": subCharts, + } + + // If there is a {{.Values.ThisChart}} in the parent metadata, + // copy that into the {{.Values}} for this template. + if accessor.IsRoot() { + next["Values"] = vals["Values"] + } else if vs, err := values.Table("Values." + accessor.Name()); err == nil { + next["Values"] = vs + } + + for _, child := range accessor.Dependencies() { + // TODO: Handle error + sub, _ := ci.NewAccessor(child) + subCharts[sub.Name()] = recAllTpls(child, templates, next) + } + + newParentID := accessor.ChartFullPath() + for _, t := range accessor.Templates() { + if t == nil { + continue + } + if !isTemplateValid(accessor, t.Name) { + continue + } + templates[path.Join(newParentID, t.Name)] = renderable{ + tpl: string(t.Data), + vals: next, + basePath: path.Join(newParentID, "templates"), + } + } + + return next +} + +// isTemplateValid returns true if the template is valid for the chart type +func isTemplateValid(accessor ci.Accessor, templateName string) bool { + if accessor.IsLibraryChart() { + return strings.HasPrefix(filepath.Base(templateName), "_") + } + return true +} + +func setupWerfSecretFile(secretsRuntimeData secretFilesRuntimeData, funcMap template.FuncMap) { + funcMap["werf_secret_file"] = func(secretRelativePath string) (string, error) { + if path.IsAbs(secretRelativePath) { + return "", fmt.Errorf("expected relative secret file path, given path %v", secretRelativePath) + } + + decodedData, ok := secretsRuntimeData.GetDecryptedSecretFilesData()[secretRelativePath] + + if !ok { + var secretFiles []string + for key := range secretsRuntimeData.GetDecryptedSecretFilesData() { + secretFiles = append(secretFiles, key) + } + + return "", fmt.Errorf("secret file %q not found, you may use one of the following: %q", secretRelativePath, strings.Join(secretFiles, "', '")) + } + + return decodedData, nil + } +} + +func secretsRuntimeDataFromChart(chrt ci.Charter) chartcommon.RuntimeData { + switch c := chrt.(type) { + case *v2.Chart: + return c.SecretsRuntimeData + case *v3.Chart: + return c.SecretsRuntimeData + default: + return nil + } +} diff --git a/pkg/helm/pkg/engine/engine_migration_ai_test.go b/pkg/helm/pkg/engine/engine_migration_ai_test.go new file mode 100644 index 00000000..56679fd8 --- /dev/null +++ b/pkg/helm/pkg/engine/engine_migration_ai_test.go @@ -0,0 +1,83 @@ +//go:build ai_tests + +package engine + +import ( + "context" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func TestAI_DebugFuncsExistInFuncMap(t *testing.T) { + e := Engine{} + tpl := template.New("test") + e.initFunMap(context.Background(), tpl) + + funcs := []string{"dump_debug", "printf_debug", "include_debug", "tpl_debug"} + for _, name := range funcs { + found := false + for _, tmpl := range tpl.Templates() { + _ = tmpl + } + testTpl := template.New("check_" + name) + testTpl.Funcs(template.FuncMap{name: func() string { return "" }}) + _ = testTpl + + execTpl, err := tpl.Parse("{{ " + name + " }}") + if err == nil && execTpl != nil { + found = true + } + assert.True(t, found, "function %q should be registered in FuncMap", name) + } +} + +func TestAI_WerfSecretFileFuncExists(t *testing.T) { + e := Engine{} + tpl := template.New("test") + e.initFunMap(context.Background(), tpl) + + parsed, err := tpl.Parse(`{{ werf_secret_file "test.txt" }}`) + require.NoError(t, err) + assert.NotNil(t, parsed) +} + +func TestAI_EngineRendersSimpleTemplate(t *testing.T) { + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "test-chart", + Version: "0.1.0", + APIVersion: chart.APIVersionV2, + }, + Templates: []*common.File{ + {Name: "templates/hello.yaml", Data: []byte("greeting: {{ .Values.hello }}")}, + }, + Values: map[string]interface{}{ + "hello": "world", + }, + } + + vals := common.Values{ + "Values": c.Values, + "Release": map[string]interface{}{"Name": "test", "Namespace": "default", "IsInstall": true, "IsUpgrade": false, "Service": "Helm"}, + "Chart": map[string]interface{}{"Name": "test-chart", "Version": "0.1.0"}, + } + + out, err := Render(context.Background(), c, vals) + require.NoError(t, err) + require.NotEmpty(t, out) + + found := false + for _, v := range out { + if v == "greeting: world" { + found = true + break + } + } + assert.True(t, found, "rendered output should contain 'greeting: world', got %v", out) +} diff --git a/pkg/helm/pkg/engine/engine_test.go b/pkg/helm/pkg/engine/engine_test.go index 71edafd9..1b3e298a 100644 --- a/pkg/helm/pkg/engine/engine_test.go +++ b/pkg/helm/pkg/engine/engine_test.go @@ -17,12 +17,16 @@ limitations under the License. package engine import ( + "context" "fmt" "path" "strings" "sync" "testing" "text/template" + "time" + + "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -30,8 +34,9 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/dynamic/fake" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + "github.com/werf/nelm/pkg/helm/pkg/chart/common/util" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" ) func TestSortTemplates(t *testing.T) { @@ -78,7 +83,7 @@ func TestFuncMap(t *testing.T) { } // Test for Engine-specific template functions. - expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "toJson", "fromJson", "lookup"} + expect := []string{"include", "required", "tpl", "toYaml", "fromYaml", "toToml", "fromToml", "toJson", "fromJson", "lookup"} for _, f := range expect { if _, ok := fns[f]; !ok { t.Errorf("Expected add-on function %q", f) @@ -87,17 +92,18 @@ func TestFuncMap(t *testing.T) { } func TestRender(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ - {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, - {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, - {Name: "templates/test3", Data: []byte("{{.noValue}}")}, - {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, - {Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + Templates: []*common.File{ + {Name: "templates/test1", ModTime: modTime, Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, + {Name: "templates/test2", ModTime: modTime, Data: []byte("{{.Values.global.callme | lower }}")}, + {Name: "templates/test3", ModTime: modTime, Data: []byte("{{.noValue}}")}, + {Name: "templates/test4", ModTime: modTime, Data: []byte("{{toJson .Values}}")}, + {Name: "templates/test5", ModTime: modTime, Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, } @@ -112,11 +118,11 @@ func TestRender(t *testing.T) { }, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Errorf("Failed to render templates: %s", err) } @@ -137,14 +143,16 @@ func TestRender(t *testing.T) { } func TestRenderRefsOrdering(t *testing.T) { + modTime := time.Now() + parentChart := &chart.Chart{ Metadata: &chart.Metadata{ Name: "parent", Version: "1.2.3", }, - Templates: []*chart.File{ - {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, - {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, + Templates: []*common.File{ + {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, + {Name: "templates/test.yaml", ModTime: modTime, Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, }, } childChart := &chart.Chart{ @@ -152,8 +160,8 @@ func TestRenderRefsOrdering(t *testing.T) { Name: "child", Version: "1.2.3", }, - Templates: []*chart.File{ - {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, + Templates: []*common.File{ + {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, }, } parentChart.AddDependency(childChart) @@ -162,8 +170,8 @@ func TestRenderRefsOrdering(t *testing.T) { "parent/templates/test.yaml": "parent value", } - for i := 0; i < 100; i++ { - out, err := Render(parentChart, chartutil.Values{}) + for i := range 100 { + out, err := Render(context.Background(), parentChart, common.Values{}) if err != nil { t.Fatalf("Failed to render templates: %s", err) } @@ -179,7 +187,7 @@ func TestRenderRefsOrdering(t *testing.T) { func TestRenderInternals(t *testing.T) { // Test the internals of the rendering tool. - vals := chartutil.Values{"Name": "one", "Value": "two"} + vals := common.Values{"Name": "one", "Value": "two"} tpls := map[string]renderable{ "one": {tpl: `Hello {{title .Name}}`, vals: vals}, "two": {tpl: `Goodbye {{upper .Value}}`, vals: vals}, @@ -188,7 +196,7 @@ func TestRenderInternals(t *testing.T) { "three": {tpl: `{{template "two" dict "Value" "three"}}`, vals: vals}, } - out, err := new(Engine).render(tpls, nil) + out, err := new(Engine).render(context.Background(), tpls) if err != nil { t.Fatalf("Failed template rendering: %s", err) } @@ -216,8 +224,8 @@ func TestRenderWithDNS(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ - {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + Templates: []*common.File{ + {Name: "templates/test1", ModTime: time.Now(), Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{}, } @@ -226,14 +234,14 @@ func TestRenderWithDNS(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } var e Engine e.EnableDNS = true - out, err := e.Render(c, v) + out, err := e.Render(context.Background(), c, v) if err != nil { t.Errorf("Failed to render templates: %s", err) } @@ -352,10 +360,12 @@ func TestRenderWithClientProvider(t *testing.T) { Values: map[string]interface{}{}, } + modTime := time.Now() for name, exp := range cases { - c.Templates = append(c.Templates, &chart.File{ - Name: path.Join("templates", name), - Data: []byte(exp.template), + c.Templates = append(c.Templates, &common.File{ + Name: path.Join("templates", name), + ModTime: modTime, + Data: []byte(exp.template), }) } @@ -363,12 +373,12 @@ func TestRenderWithClientProvider(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - out, err := RenderWithClientProvider(c, v, provider) + out, err := RenderWithClientProvider(context.Background(), c, v, provider) if err != nil { t.Errorf("Failed to render templates: %s", err) } @@ -389,8 +399,8 @@ func TestRenderWithClientProvider_error(t *testing.T) { Name: "moby", Version: "1.2.3", }, - Templates: []*chart.File{ - {Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, + Templates: []*common.File{ + {Name: "templates/error", ModTime: time.Now(), Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, }, Values: map[string]interface{}{}, } @@ -399,7 +409,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { "Values": map[string]interface{}{}, } - v, err := chartutil.CoalesceValues(c, vals) + v, err := util.CoalesceValues(c, vals) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } @@ -412,7 +422,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { }, }, } - _, err = RenderWithClientProvider(c, v, provider) + _, err = RenderWithClientProvider(context.Background(), c, v, provider) if err == nil || !strings.Contains(err.Error(), "kaboom") { t.Errorf("Expected error from client provider when rendering, got %q", err) } @@ -422,7 +432,7 @@ func TestParallelRenderInternals(t *testing.T) { // Make sure that we can use one Engine to run parallel template renders. e := new(Engine) var wg sync.WaitGroup - for i := 0; i < 20; i++ { + for i := range 20 { wg.Add(1) go func(i int) { tt := fmt.Sprintf("expect-%d", i) @@ -432,7 +442,7 @@ func TestParallelRenderInternals(t *testing.T) { vals: map[string]interface{}{"val": tt}, }, } - out, err := e.render(tpls, nil) + out, err := e.render(context.Background(), tpls) if err != nil { t.Errorf("Failed to render %s: %s", tt, err) } @@ -446,12 +456,12 @@ func TestParallelRenderInternals(t *testing.T) { } func TestParseErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} tplsUndefinedFunction := map[string]renderable{ "undefined_function": {tpl: `{{foo}}`, vals: vals}, } - _, err := new(Engine).render(tplsUndefinedFunction, nil) + _, err := new(Engine).render(context.Background(), tplsUndefinedFunction) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -462,7 +472,7 @@ func TestParseErrors(t *testing.T) { } func TestExecErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} cases := []struct { name string tpls map[string]renderable @@ -514,7 +524,7 @@ linebreak`, for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - _, err := new(Engine).render(tt.tpls, nil) + _, err := new(Engine).render(context.Background(), tt.tpls) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -526,13 +536,13 @@ linebreak`, } func TestFailErrors(t *testing.T) { - vals := chartutil.Values{"Values": map[string]interface{}{}} + vals := common.Values{"Values": map[string]interface{}{}} failtpl := `All your base are belong to us{{ fail "This is an error" }}` tplsFailed := map[string]renderable{ "failtpl": {tpl: failtpl, vals: vals}, } - _, err := new(Engine).render(tplsFailed, nil) + _, err := new(Engine).render(context.Background(), tplsFailed) if err == nil { t.Fatalf("Expected failures while rendering: %s", err) } @@ -543,7 +553,7 @@ func TestFailErrors(t *testing.T) { var e Engine e.LintMode = true - out, err := e.render(tplsFailed, nil) + out, err := e.render(context.Background(), tplsFailed) if err != nil { t.Fatal(err) } @@ -555,52 +565,54 @@ func TestFailErrors(t *testing.T) { } func TestAllTemplates(t *testing.T) { + modTime := time.Now() ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, - Templates: []*chart.File{ - {Name: "templates/foo", Data: []byte("foo")}, - {Name: "templates/bar", Data: []byte("bar")}, + Templates: []*common.File{ + {Name: "templates/foo", ModTime: modTime, Data: []byte("foo")}, + {Name: "templates/bar", ModTime: modTime, Data: []byte("bar")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "laboratory mice"}, - Templates: []*chart.File{ - {Name: "templates/pinky", Data: []byte("pinky")}, - {Name: "templates/brain", Data: []byte("brain")}, + Templates: []*common.File{ + {Name: "templates/pinky", ModTime: modTime, Data: []byte("pinky")}, + {Name: "templates/brain", ModTime: modTime, Data: []byte("brain")}, }, } ch1.AddDependency(dep1) dep2 := &chart.Chart{ Metadata: &chart.Metadata{Name: "same thing we do every night"}, - Templates: []*chart.File{ - {Name: "templates/innermost", Data: []byte("innermost")}, + Templates: []*common.File{ + {Name: "templates/innermost", ModTime: modTime, Data: []byte("innermost")}, }, } dep1.AddDependency(dep2) - tpls := allTemplates(ch1, chartutil.Values{}) + tpls := allTemplates(ch1, common.Values{}) if len(tpls) != 5 { t.Errorf("Expected 5 charts, got %d", len(tpls)) } } func TestChartValuesContainsIsRoot(t *testing.T) { + modTime := time.Now() ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "parent"}, - Templates: []*chart.File{ - {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + Templates: []*common.File{ + {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "child"}, - Templates: []*chart.File{ - {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + Templates: []*common.File{ + {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")}, }, } ch1.AddDependency(dep1) - out, err := Render(ch1, chartutil.Values{}) + out, err := Render(context.Background(), ch1, common.Values{}) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -618,20 +630,21 @@ func TestChartValuesContainsIsRoot(t *testing.T) { func TestRenderDependency(t *testing.T) { deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` + modTime := time.Now() ch := &chart.Chart{ Metadata: &chart.Metadata{Name: "outerchart"}, - Templates: []*chart.File{ - {Name: "templates/outer", Data: []byte(toptpl)}, + Templates: []*common.File{ + {Name: "templates/outer", ModTime: modTime, Data: []byte(toptpl)}, }, } ch.AddDependency(&chart.Chart{ Metadata: &chart.Metadata{Name: "innerchart"}, - Templates: []*chart.File{ - {Name: "templates/inner", Data: []byte(deptpl)}, + Templates: []*common.File{ + {Name: "templates/inner", ModTime: modTime, Data: []byte(deptpl)}, }, }) - out, err := Render(ch, map[string]interface{}{}) + out, err := Render(context.Background(), ch, map[string]interface{}{}) if err != nil { t.Fatalf("failed to render chart: %s", err) } @@ -656,19 +669,20 @@ func TestRenderNestedValues(t *testing.T) { // Ensure subcharts scopes are working. subchartspath := "templates/subcharts.tpl" + modTime := time.Now() deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, - Templates: []*chart.File{ - {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, - {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, + Templates: []*common.File{ + {Name: deepestpath, ModTime: modTime, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, + {Name: checkrelease, ModTime: modTime, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, }, Values: map[string]interface{}{"what": "milkshake", "where": "here"}, } inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "herrick"}, - Templates: []*chart.File{ - {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, + Templates: []*common.File{ + {Name: innerpath, ModTime: modTime, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, } @@ -676,9 +690,9 @@ func TestRenderNestedValues(t *testing.T) { outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "top"}, - Templates: []*chart.File{ - {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, - {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, + Templates: []*common.File{ + {Name: outerpath, ModTime: modTime, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, + {Name: subchartspath, ModTime: modTime, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, }, Values: map[string]interface{}{ "what": "stinkweed", @@ -704,22 +718,22 @@ func TestRenderNestedValues(t *testing.T) { }, } - tmp, err := chartutil.CoalesceValues(outer, injValues) + tmp, err := util.CoalesceValues(outer, injValues) if err != nil { t.Fatalf("Failed to coalesce values: %s", err) } - inject := chartutil.Values{ + inject := common.Values{ "Values": tmp, "Chart": outer.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "dyin", }, } t.Logf("Calculated values: %v", inject) - out, err := Render(outer, inject) + out, err := Render(context.Background(), outer, inject) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -751,38 +765,39 @@ func TestRenderNestedValues(t *testing.T) { } func TestRenderBuiltinValues(t *testing.T) { + modTime := time.Now() inner := &chart.Chart{ - Metadata: &chart.Metadata{Name: "Latium"}, - Templates: []*chart.File{ - {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, + Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2}, + Templates: []*common.File{ + {Name: "templates/Lavinia", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/From", ModTime: modTime, Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, - Files: []*chart.File{ - {Name: "author", Data: []byte("Virgil")}, - {Name: "book/title.txt", Data: []byte("Aeneid")}, + Files: []*common.File{ + {Name: "author", ModTime: modTime, Data: []byte("Virgil")}, + {Name: "book/title.txt", ModTime: modTime, Data: []byte("Aeneid")}, }, } outer := &chart.Chart{ - Metadata: &chart.Metadata{Name: "Troy"}, - Templates: []*chart.File{ - {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, + Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2}, + Templates: []*common.File{ + {Name: "templates/Aeneas", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/Amata", ModTime: modTime, Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, }, } outer.AddDependency(inner) - inject := chartutil.Values{ + inject := common.Values{ "Values": "", "Chart": outer.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "Aeneid", }, } t.Logf("Calculated values: %v", outer) - out, err := Render(outer, inject) + out, err := Render(context.Background(), outer, inject) if err != nil { t.Fatalf("failed to render templates: %s", err) } @@ -802,32 +817,33 @@ func TestRenderBuiltinValues(t *testing.T) { } func TestAlterFuncMap_include(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conrad"}, - Templates: []*chart.File{ - {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, - {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + Templates: []*common.File{ + {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)}, }, } // Check nested reference in include FuncMap d := &chart.Chart{ Metadata: &chart.Metadata{Name: "nested"}, - Templates: []*chart.File{ - {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, - {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + Templates: []*common.File{ + {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Values": "", "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "Mistah Kurtz", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -837,7 +853,7 @@ func TestAlterFuncMap_include(t *testing.T) { t.Errorf("Expected %q, got %q (%v)", expect, got, out) } - _, err = Render(d, v) + _, err = Render(context.Background(), d, v) expectErrName := "nested/templates/quote" if err == nil { t.Errorf("Expected err of nested reference name: %v", expectErrName) @@ -845,26 +861,27 @@ func TestAlterFuncMap_include(t *testing.T) { } func TestAlterFuncMap_require(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conan"}, - Templates: []*chart.File{ - {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, - {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, + Templates: []*common.File{ + {Name: "templates/quote", ModTime: modTime, Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, + {Name: "templates/bases", ModTime: modTime, Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "who": "us", "bases": 2, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "That 90s meme", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -880,18 +897,18 @@ func TestAlterFuncMap_require(t *testing.T) { // test required without passing in needed values with lint mode on // verifies lint replaces required with an empty string (should not fail) - lintValues := chartutil.Values{ - "Values": chartutil.Values{ + lintValues := common.Values{ + "Values": common.Values{ "who": "us", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "That 90s meme", }, } var e Engine e.LintMode = true - out, err = e.Render(c, lintValues) + out, err = e.Render(context.Background(), c, lintValues) if err != nil { t.Fatal(err) } @@ -909,22 +926,22 @@ func TestAlterFuncMap_require(t *testing.T) { func TestAlterFuncMap_tpl(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ - {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, + Templates: []*common.File{ + {Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -938,22 +955,22 @@ func TestAlterFuncMap_tpl(t *testing.T) { func TestAlterFuncMap_tplfunc(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ - {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, + Templates: []*common.File{ + {Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -965,24 +982,25 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { } func TestAlterFuncMap_tplinclude(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, - Templates: []*chart.File{ - {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, - {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, + Templates: []*common.File{ + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Template.Name}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "value": "myvalue", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -995,24 +1013,26 @@ func TestAlterFuncMap_tplinclude(t *testing.T) { } func TestRenderRecursionLimit(t *testing.T) { + modTime := time.Now() + // endless recursion should produce an error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "bad"}, - Templates: []*chart.File{ - {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, - {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, + Templates: []*common.File{ + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{include "recursion" . }}`)}, + {Name: "templates/recursion", ModTime: modTime, Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Values": "", "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } expectErr := "rendering template has a nested reference name: recursion: unable to execute template" - _, err := Render(c, v) + _, err := Render(context.Background(), c, v) if err == nil || !strings.HasSuffix(err.Error(), expectErr) { t.Errorf("Expected err with suffix: %s", expectErr) } @@ -1021,26 +1041,26 @@ func TestRenderRecursionLimit(t *testing.T) { times := 4000 phrase := "All work and no play makes Jack a dull boy" printFunc := `{{define "overlook"}}{{printf "` + phrase + `\n"}}{{end}}` - var repeatedIncl string - for i := 0; i < times; i++ { - repeatedIncl += `{{include "overlook" . }}` + var repeatedIncl strings.Builder + for range times { + repeatedIncl.WriteString(`{{include "overlook" . }}`) } d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, - Templates: []*chart.File{ - {Name: "templates/quote", Data: []byte(repeatedIncl)}, - {Name: "templates/_function", Data: []byte(printFunc)}, + Templates: []*common.File{ + {Name: "templates/quote", ModTime: modTime, Data: []byte(repeatedIncl.String())}, + {Name: "templates/_function", ModTime: modTime, Data: []byte(printFunc)}, }, } - out, err := Render(d, v) + out, err := Render(context.Background(), d, v) if err != nil { t.Fatal(err) } var expect string - for i := 0; i < times; i++ { + for range times { expect += phrase + "\n" } if got := out["overlook/templates/quote"]; got != expect { @@ -1050,30 +1070,31 @@ func TestRenderRecursionLimit(t *testing.T) { } func TestRenderLoadTemplateForTplFromFile(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, - Templates: []*chart.File{ - {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, - {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, + Templates: []*common.File{ + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, + {Name: "templates/_function", ModTime: modTime, Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, }, - Files: []*chart.File{ - {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, - {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, + Files: []*common.File{ + {Name: "test", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, + {Name: "test2", ModTime: modTime, Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "filename": "test", "filename2": "test2", }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -1085,22 +1106,23 @@ func TestRenderLoadTemplateForTplFromFile(t *testing.T) { } func TestRenderTplEmpty(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplEmpty"}, - Templates: []*chart.File{ - {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, - {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, - {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, + Templates: []*common.File{ + {Name: "templates/empty-string", ModTime: modTime, Data: []byte(`{{tpl "" .}}`)}, + {Name: "templates/empty-action", ModTime: modTime, Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, + {Name: "templates/only-defines", ModTime: modTime, Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, }, } - v := chartutil.Values{ + v := common.Values{ "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -1118,21 +1140,22 @@ func TestRenderTplEmpty(t *testing.T) { } func TestRenderTplTemplateNames(t *testing.T) { + modTime := time.Now() // .Template.BasePath and .Name make it through c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplTemplateNames"}, - Templates: []*chart.File{ - {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, - {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, - {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, - {Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, - {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, - }, - } - v := chartutil.Values{ - "Values": chartutil.Values{ - "dot": chartutil.Values{ - "Template": chartutil.Values{ + Templates: []*common.File{ + {Name: "templates/default-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, + {Name: "templates/default-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, + {Name: "templates/modified-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, + {Name: "templates/modified-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, + {Name: "templates/modified-field", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, + }, + } + v := common.Values{ + "Values": common.Values{ + "dot": common.Values{ + "Template": common.Values{ "BasePath": "path/to/template", "Name": "name-of-template", "Field": "extra-field", @@ -1140,12 +1163,12 @@ func TestRenderTplTemplateNames(t *testing.T) { }, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -1165,12 +1188,13 @@ func TestRenderTplTemplateNames(t *testing.T) { } func TestRenderTplRedefines(t *testing.T) { + modTime := time.Now() // Redefining a template inside 'tpl' does not affect the outer definition c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplRedefines"}, - Templates: []*chart.File{ - {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, - {Name: "templates/partial", Data: []byte( + Templates: []*common.File{ + {Name: "templates/_partials", ModTime: modTime, Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, + {Name: "templates/partial", ModTime: modTime, Data: []byte( `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, )}, {Name: "templates/manifest", Data: []byte( @@ -1190,8 +1214,8 @@ func TestRenderTplRedefines(t *testing.T) { )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{ + v := common.Values{ + "Values": common.Values{ "partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`, "manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`, "manifestOnlyText": `tpl: {{include "manifest-only" .}}`, @@ -1203,12 +1227,12 @@ func TestRenderTplRedefines(t *testing.T) { "innerText": `{{define "nested"}}redefined-in-inner-tpl{{end}}inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`, }, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -1234,21 +1258,21 @@ func TestRenderTplMissingKey(t *testing.T) { // Rendering a missing key results in empty/zero output. c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKey"}, - Templates: []*chart.File{ - {Name: "templates/manifest", Data: []byte( + Templates: []*common.File{ + {Name: "templates/manifest", ModTime: time.Now(), Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{}, + v := common.Values{ + "Values": common.Values{}, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } - out, err := Render(c, v) + out, err := Render(context.Background(), c, v) if err != nil { t.Fatal(err) } @@ -1267,16 +1291,16 @@ func TestRenderTplMissingKeyString(t *testing.T) { // Rendering a missing key results in error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, - Templates: []*chart.File{ - {Name: "templates/manifest", Data: []byte( + Templates: []*common.File{ + {Name: "templates/manifest", ModTime: time.Now(), Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, } - v := chartutil.Values{ - "Values": chartutil.Values{}, + v := common.Values{ + "Values": common.Values{}, "Chart": c.Metadata, - "Release": chartutil.Values{ + "Release": common.Values{ "Name": "TestRelease", }, } @@ -1284,19 +1308,198 @@ func TestRenderTplMissingKeyString(t *testing.T) { e := new(Engine) e.Strict = true - out, err := e.Render(c, v) + out, err := e.Render(context.Background(), c, v) if err == nil { t.Errorf("Expected error, got %v", out) return } - switch err.(type) { - case (template.ExecError): - errTxt := fmt.Sprint(err) - if !strings.Contains(errTxt, "noSuchKey") { - t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt) - } - default: - // Some unexpected error. + errTxt := fmt.Sprint(err) + if !strings.Contains(errTxt, "noSuchKey") { + t.Errorf("Expected error to contain 'noSuchKey', got %s", errTxt) + } + +} + +func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { + modTime := time.Now() + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "NestedHelperFunctions"}, + Templates: []*common.File{ + {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte( + `name: {{ include "nested_helper.name" . }}`, + )}, + {Name: "templates/_helpers_1.tpl", ModTime: modTime, Data: []byte( + `{{- define "nested_helper.name" -}}{{- include "common.names.get_name" . -}}{{- end -}}`, + )}, + {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte( + `{{- define "common.names.get_name" -}}{{- .Values.nonexistant.key | trunc 63 | trimSuffix "-" -}}{{- end -}}`, + )}, + }, + } + + expectedErrorMessage := `NestedHelperFunctions/templates/svc.yaml:1:9 + executing "NestedHelperFunctions/templates/svc.yaml" at : + error calling include: +NestedHelperFunctions/templates/_helpers_1.tpl:1:39 + executing "nested_helper.name" at : + error calling include: +NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 + executing "common.names.get_name" at <.Values.nonexistant.key>: + nil pointer evaluating interface {}.key` + + v := common.Values{} + + val, _ := util.CoalesceValues(c, v) + vals := map[string]interface{}{ + "Values": val.AsMap(), + } + _, err := Render(context.Background(), c, vals) + + assert.NotNil(t, err) + assert.Equal(t, expectedErrorMessage, err.Error()) +} + +func TestMultilineNoTemplateAssociatedError(t *testing.T) { + modTime := time.Now() + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "multiline"}, + Templates: []*common.File{ + {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte( + `name: {{ include "nested_helper.name" . }}`, + )}, + {Name: "templates/test.yaml", ModTime: modTime, Data: []byte( + `{{ toYaml .Values }}`, + )}, + {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte( + `{{ toYaml .Values }}`, + )}, + }, + } + + expectedErrorMessage := `multiline/templates/svc.yaml:1:9 + executing "multiline/templates/svc.yaml" at : + error calling include: +template: no template "nested_helper.name" associated with template "gotpl"` + + v := common.Values{} + + val, _ := util.CoalesceValues(c, v) + vals := map[string]interface{}{ + "Values": val.AsMap(), + } + _, err := Render(context.Background(), c, vals) + + assert.NotNil(t, err) + assert.Equal(t, expectedErrorMessage, err.Error()) +} + +func TestRenderCustomTemplateFuncs(t *testing.T) { + modTime := time.Now() + + // Create a chart with two templates that use custom functions + c := &chart.Chart{ + Metadata: &chart.Metadata{Name: "CustomFunc"}, + Templates: []*common.File{ + { + Name: "templates/manifest", + ModTime: modTime, + Data: []byte(`{{exclaim .Values.message}}`), + }, + { + Name: "templates/override", + ModTime: modTime, + Data: []byte(`{{ upper .Values.message }}`), + }, + }, + } + v := common.Values{ + "Values": common.Values{ + "message": "hello", + }, + "Chart": c.Metadata, + "Release": common.Values{ + "Name": "TestRelease", + }, + } + + // Define a custom template function "exclaim" that appends "!!!" to a string and override "upper" function + customFuncs := template.FuncMap{ + "exclaim": func(input string) string { + return input + "!!!" + }, + "upper": func(s string) string { + return "custom:" + s + }, + } + + // Create an engine instance and set the CustomTemplateFuncs. + e := new(Engine) + e.CustomTemplateFuncs = customFuncs + + // Render the chart. + out, err := e.Render(context.Background(), c, v) + if err != nil { t.Fatal(err) } + + // Expected output should be "hello!!!". + expected := "hello!!!" + key := "CustomFunc/templates/manifest" + if rendered, ok := out[key]; !ok || rendered != expected { + t.Errorf("Expected %q, got %q", expected, rendered) + } + + // Verify that the rendered template used the custom "upper" function. + expected = "custom:hello" + key = "CustomFunc/templates/override" + if rendered, ok := out[key]; !ok || rendered != expected { + t.Errorf("Expected %q, got %q", expected, rendered) + } +} + +func TestTraceableError_SimpleForm(t *testing.T) { + testStrings := []string{ + "function_not_found/templates/secret.yaml: error calling include", + } + for _, errString := range testStrings { + trace, done := parseTemplateSimpleErrorString(errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.message != "error calling include" { + t.Errorf("Expected %q, got %q", errString, trace.message) + } + } +} +func TestTraceableError_ExecutingForm(t *testing.T) { + testStrings := [][]string{ + {"function_not_found/templates/secret.yaml:6:11: executing \"function_not_found/templates/secret.yaml\" at : ", "function_not_found/templates/secret.yaml:6:11"}, + {"divide_by_zero/templates/secret.yaml:6:11: executing \"divide_by_zero/templates/secret.yaml\" at : ", "divide_by_zero/templates/secret.yaml:6:11"}, + } + for _, errTuple := range testStrings { + errString := errTuple[0] + expectedLocation := errTuple[1] + trace, done := parseTemplateExecutingAtErrorType(errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.location != expectedLocation { + t.Errorf("Expected %q, got %q", expectedLocation, trace.location) + } + } +} + +func TestTraceableError_NoTemplateForm(t *testing.T) { + testStrings := []string{ + "no template \"common.names.get_name\" associated with template \"gotpl\"", + } + for _, errString := range testStrings { + trace, done := parseTemplateNoTemplateError(errString, errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.message != errString { + t.Errorf("Expected %q, got %q", errString, trace.message) + } + } } diff --git a/pkg/helm/pkg/engine/files.go b/pkg/helm/pkg/engine/files.go index 8c78951b..02a2b87d 100644 --- a/pkg/helm/pkg/engine/files.go +++ b/pkg/helm/pkg/engine/files.go @@ -23,7 +23,7 @@ import ( "github.com/gobwas/glob" - "github.com/werf/nelm/pkg/helm/pkg/chart" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" ) // files is a map of files in a chart that can be accessed from a template. @@ -31,7 +31,7 @@ type files map[string][]byte // NewFiles creates a new files from chart files. // Given an []*chart.File (the format for files in a chart.Chart), extract a map of files. -func newFiles(from []*chart.File) files { +func newFiles(from []*common.File) files { files := make(map[string][]byte) for _, f := range from { files[f.Name] = f.Data @@ -64,7 +64,7 @@ func (f files) Get(name string) string { } // Glob takes a glob pattern and returns another files object only containing -// matched files. +// matched files. // // This is designed to be called from a template. // diff --git a/pkg/helm/pkg/engine/funcs.go b/pkg/helm/pkg/engine/funcs.go index 8f05a3a1..a97f8f10 100644 --- a/pkg/helm/pkg/engine/funcs.go +++ b/pkg/helm/pkg/engine/funcs.go @@ -19,12 +19,14 @@ package engine import ( "bytes" "encoding/json" + "maps" "strings" "text/template" "github.com/BurntSushi/toml" "github.com/Masterminds/sprig/v3" "sigs.k8s.io/yaml" + goYaml "sigs.k8s.io/yaml/goyaml.v3" ) // funcMap returns a mapping of all of the functions that Engine has. @@ -48,10 +50,14 @@ func funcMap() template.FuncMap { // Add some extra functionality extra := template.FuncMap{ "toToml": toTOML, + "fromToml": fromTOML, "toYaml": toYAML, + "mustToYaml": mustToYAML, + "toYamlPretty": toYAMLPretty, "fromYaml": fromYAML, "fromYamlArray": fromYAMLArray, "toJson": toJSON, + "mustToJson": mustToJSON, "fromJson": fromJSON, "fromJsonArray": fromJSONArray, @@ -68,9 +74,7 @@ func funcMap() template.FuncMap { }, } - for k, v := range extra { - f[k] = v - } + maps.Copy(f, extra) return f } @@ -88,6 +92,32 @@ func toYAML(v interface{}) string { return strings.TrimSuffix(string(data), "\n") } +// mustToYAML takes an interface, marshals it to yaml, and returns a string. +// It will panic if there is an error. +// +// This is designed to be called from a template when need to ensure that the +// output YAML is valid. +func mustToYAML(v interface{}) string { + data, err := yaml.Marshal(v) + if err != nil { + panic(err) + } + return strings.TrimSuffix(string(data), "\n") +} + +func toYAMLPretty(v interface{}) string { + var data bytes.Buffer + encoder := goYaml.NewEncoder(&data) + encoder.SetIndent(2) + err := encoder.Encode(v) + + if err != nil { + // Swallow errors inside of a template. + return "" + } + return strings.TrimSuffix(data.String(), "\n") +} + // fromYAML converts a YAML document into a map[string]interface{}. // // This is not a general-purpose YAML parser, and will not parse all valid @@ -132,6 +162,21 @@ func toTOML(v interface{}) string { return b.String() } +// fromTOML converts a TOML document into a map[string]interface{}. +// +// This is not a general-purpose TOML parser, and will not parse all valid +// TOML documents. Additionally, because its intended use is within templates +// it tolerates errors. It will insert the returned error message string into +// m["Error"] in the returned map. +func fromTOML(str string) map[string]interface{} { + m := make(map[string]interface{}) + + if err := toml.Unmarshal([]byte(str), &m); err != nil { + m["Error"] = err.Error() + } + return m +} + // toJSON takes an interface, marshals it to json, and returns a string. It will // always return a string, even on marshal error (empty string). // @@ -145,6 +190,19 @@ func toJSON(v interface{}) string { return string(data) } +// mustToJSON takes an interface, marshals it to json, and returns a string. +// It will panic if there is an error. +// +// This is designed to be called from a template when need to ensure that the +// output JSON is valid. +func mustToJSON(v interface{}) string { + data, err := json.Marshal(v) + if err != nil { + panic(err) + } + return string(data) +} + // fromJSON converts a JSON document into a map[string]interface{}. // // This is not a general-purpose JSON parser, and will not parse all valid diff --git a/pkg/helm/pkg/engine/funcs_test.go b/pkg/helm/pkg/engine/funcs_test.go index 29bc121b..71a72e2e 100644 --- a/pkg/helm/pkg/engine/funcs_test.go +++ b/pkg/helm/pkg/engine/funcs_test.go @@ -33,10 +33,38 @@ func TestFuncs(t *testing.T) { tpl: `{{ toYaml . }}`, expect: `foo: bar`, vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ toYamlPretty . }}`, + expect: "baz:\n - 1\n - 2\n - 3", + vars: map[string]interface{}{"baz": []int{1, 2, 3}}, }, { tpl: `{{ toToml . }}`, expect: "foo = \"bar\"\n", vars: map[string]interface{}{"foo": "bar"}, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[hello:world]", + vars: `hello = "world"`, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[table:map[keyInTable:valueInTable subtable:map[keyInSubtable:valueInSubTable]]]", + vars: ` +[table] +keyInTable = "valueInTable" +[table.subtable] +keyInSubtable = "valueInSubTable"`, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[tableArray:[map[keyInElement0:valueInElement0] map[keyInElement1:valueInElement1]]]", + vars: ` +[[tableArray]] +keyInElement0 = "valueInElement0" +[[tableArray]] +keyInElement1 = "valueInElement1"`, + }, { + tpl: `{{ fromToml . }}`, + expect: "map[Error:toml: line 1: unexpected EOF; expected key separator '=']", + vars: "one", }, { tpl: `{{ toJson . }}`, expect: `{"foo":"bar"}`, @@ -107,6 +135,43 @@ func TestFuncs(t *testing.T) { assert.NoError(t, err) assert.Equal(t, tt.expect, b.String(), tt.tpl) } + + loopMap := map[string]interface{}{ + "foo": "bar", + } + loopMap["loop"] = []interface{}{loopMap} + + mustFuncsTests := []struct { + tpl string + expect interface{} + vars interface{} + }{{ + tpl: `{{ mustToYaml . }}`, + vars: loopMap, + }, { + tpl: `{{ mustToJson . }}`, + vars: loopMap, + }, { + tpl: `{{ toYaml . }}`, + expect: "", // should return empty string and swallow error + vars: loopMap, + }, { + tpl: `{{ toJson . }}`, + expect: "", // should return empty string and swallow error + vars: loopMap, + }, + } + + for _, tt := range mustFuncsTests { + var b strings.Builder + err := template.Must(template.New("test").Funcs(funcMap()).Parse(tt.tpl)).Execute(&b, tt.vars) + if tt.expect != nil { + assert.NoError(t, err) + assert.Equal(t, tt.expect, b.String(), tt.tpl) + } else { + assert.Error(t, err) + } + } } // This test to check a function provided by sprig is due to a change in a diff --git a/pkg/helm/pkg/engine/lookup_func.go b/pkg/helm/pkg/engine/lookup_func.go index 86a7d698..c6ad8d25 100644 --- a/pkg/helm/pkg/engine/lookup_func.go +++ b/pkg/helm/pkg/engine/lookup_func.go @@ -18,10 +18,10 @@ package engine import ( "context" - "log" + "fmt" + "log/slog" "strings" - "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" @@ -35,10 +35,7 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam // NewLookupFunction returns a function for looking up objects in the cluster. // // If the resource does not exist, no error is raised. -// -// This function is considered deprecated, and will be renamed in Helm 4. It will no -// longer be a public function. -func NewLookupFunction(config *rest.Config) lookupFunc { +func NewLookupFunction(config *rest.Config) lookupFunc { //nolint:revive return newLookupFunction(clientProviderFromConfig{config: config}) } @@ -101,8 +98,12 @@ func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) gvk := schema.FromAPIVersionAndKind(apiversion, kind) apiRes, err := getAPIResourceForGVK(gvk, config) if err != nil { - log.Printf("[ERROR] unable to get apiresource from unstructured: %s , error %s", gvk.String(), err) - return nil, false, errors.Wrapf(err, "unable to get apiresource from unstructured: %s", gvk.String()) + slog.Error( + "unable to get apiresource", + slog.String("groupVersionKind", gvk.String()), + slog.Any("error", err), + ) + return nil, false, fmt.Errorf("unable to get apiresource from unstructured: %s: %w", gvk.String(), err) } gvr := schema.GroupVersionResource{ Group: apiRes.Group, @@ -111,7 +112,7 @@ func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) } intf, err := dynamic.NewForConfig(config) if err != nil { - log.Printf("[ERROR] unable to get dynamic client %s", err) + slog.Error("unable to get dynamic client", slog.Any("error", err)) return nil, false, err } res := intf.Resource(gvr) @@ -122,16 +123,20 @@ func getAPIResourceForGVK(gvk schema.GroupVersionKind, config *rest.Config) (met res := metav1.APIResource{} discoveryClient, err := discovery.NewDiscoveryClientForConfig(config) if err != nil { - log.Printf("[ERROR] unable to create discovery client %s", err) + slog.Error("unable to create discovery client", slog.Any("error", err)) return res, err } resList, err := discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { - log.Printf("[ERROR] unable to retrieve resource list for: %s , error: %s", gvk.GroupVersion().String(), err) + slog.Error( + "unable to retrieve resource list", + slog.String("GroupVersion", gvk.GroupVersion().String()), + slog.Any("error", err), + ) return res, err } for _, resource := range resList.APIResources { - // if a resource contains a "/" it's referencing a subresource. we don't support suberesource for now. + // if a resource contains a "/" it's referencing a subresource. we don't support subresource for now. if resource.Kind == gvk.Kind && !strings.Contains(resource.Name, "/") { res = resource res.Group = gvk.Group diff --git a/pkg/helm/pkg/errs/errors.go b/pkg/helm/pkg/errs/errors.go deleted file mode 100644 index a6944517..00000000 --- a/pkg/helm/pkg/errs/errors.go +++ /dev/null @@ -1,52 +0,0 @@ -package errs - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" -) - -func FormatTemplatingError(err error) error { - if err == nil || !strings.HasPrefix(err.Error(), "template: ") { - return err - } - - var errorsMsgs []string - currentErr := err - for currentErr != nil { - unwrapped := errors.Unwrap(currentErr) - if unwrapped != nil { - currentErr = unwrapped - } else { - currentErr = nil - continue - } - - if len(errorsMsgs) > 0 && errorsMsgs[len(errorsMsgs)-1] == unwrapped.Error() { - continue - } - - errorsMsgs = append(errorsMsgs, unwrapped.Error()) - } - - var errParts []string - for i := 0; i < len(errorsMsgs); i++ { - if i+1 > len(errorsMsgs)-1 { - errParts = append(errParts, errorsMsgs[i]) - } else { - errParts = append(errParts, strings.TrimSuffix(strings.TrimSpace(strings.TrimSuffix(errorsMsgs[i], errorsMsgs[i+1])), ":")) - } - } - - var result error - for i := len(errParts) - 1; i >= 0; i-- { - if i == len(errParts)-1 { - result = errors.New(fmt.Sprintf("\n %s", errParts[i])) - } else { - result = errors.Wrap(result, fmt.Sprintf("\n %s", errParts[i])) - } - } - - return result -} diff --git a/pkg/helm/pkg/gates/gates_test.go b/pkg/helm/pkg/gates/gates_test.go index 6bdd17ed..4d77199e 100644 --- a/pkg/helm/pkg/gates/gates_test.go +++ b/pkg/helm/pkg/gates/gates_test.go @@ -23,14 +23,13 @@ import ( const name string = "HELM_EXPERIMENTAL_FEATURE" func TestIsEnabled(t *testing.T) { - os.Unsetenv(name) g := Gate(name) if g.IsEnabled() { t.Errorf("feature gate shows as available, but the environment variable %s was not set", name) } - os.Setenv(name, "1") + t.Setenv(name, "1") if !g.IsEnabled() { t.Errorf("feature gate shows as disabled, but the environment variable %s was set", name) diff --git a/pkg/helm/pkg/getter/exports.go b/pkg/helm/pkg/getter/exports.go deleted file mode 100644 index 09d96ccd..00000000 --- a/pkg/helm/pkg/getter/exports.go +++ /dev/null @@ -1,6 +0,0 @@ -package getter - -var ( - HttpProvider = httpProvider - OCIProvider = ociProvider -) diff --git a/pkg/helm/pkg/getter/getter.go b/pkg/helm/pkg/getter/getter.go index 0484d6cd..f67fd406 100644 --- a/pkg/helm/pkg/getter/getter.go +++ b/pkg/helm/pkg/getter/getter.go @@ -18,19 +18,20 @@ package getter import ( "bytes" + "fmt" "net/http" + "slices" "time" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/cli" "github.com/werf/nelm/pkg/helm/pkg/registry" ) -// options are generic parameters to be provided to the getter during instantiation. +// getterOptions are generic parameters to be provided to the getter during instantiation. // // Getters may or may not ignore these parameters as they are passed in. -type options struct { +// TODO what is the difference between this and schema.GetterOptionsV1? +type getterOptions struct { url string certFile string keyFile string @@ -38,6 +39,7 @@ type options struct { unTar bool insecureSkipVerifyTLS bool plainHTTP bool + acceptHeader string username string password string passCredentialsAll bool @@ -46,51 +48,59 @@ type options struct { registryClient *registry.Client timeout time.Duration transport *http.Transport + artifactType string } // Option allows specifying various settings configurable by the user for overriding the defaults // used when performing Get operations with the Getter. -type Option func(*options) +type Option func(*getterOptions) // WithURL informs the getter the server name that will be used when fetching objects. Used in conjunction with // WithTLSClientConfig to set the TLSClientConfig's server name. func WithURL(url string) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.url = url } } +// WithAcceptHeader sets the request's Accept header as some REST APIs serve multiple content types +func WithAcceptHeader(header string) Option { + return func(opts *getterOptions) { + opts.acceptHeader = header + } +} + // WithBasicAuth sets the request's Authorization header to use the provided credentials func WithBasicAuth(username, password string) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.username = username opts.password = password } } func WithPassCredentialsAll(pass bool) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.passCredentialsAll = pass } } // WithUserAgent sets the request's User-Agent header to use the provided agent name. func WithUserAgent(userAgent string) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.userAgent = userAgent } } // WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS } } // WithTLSClientConfig sets the client auth with the provided credentials. func WithTLSClientConfig(certFile, keyFile, caFile string) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.certFile = certFile opts.keyFile = keyFile opts.caFile = caFile @@ -98,43 +108,50 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) Option { } func WithPlainHTTP(plainHTTP bool) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.plainHTTP = plainHTTP } } // WithTimeout sets the timeout for requests func WithTimeout(timeout time.Duration) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.timeout = timeout } } func WithTagName(tagname string) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.version = tagname } } func WithRegistryClient(client *registry.Client) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.registryClient = client } } func WithUntar() Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.unTar = true } } // WithTransport sets the http.Transport to allow overwriting the HTTPGetter default. func WithTransport(transport *http.Transport) Option { - return func(opts *options) { + return func(opts *getterOptions) { opts.transport = transport } } +// WithArtifactType sets the type of OCI artifact ("chart" or "plugin") +func WithArtifactType(artifactType string) Option { + return func(opts *getterOptions) { + opts.artifactType = artifactType + } +} + // Getter is an interface to support GET to the specified URL. type Getter interface { // Get file content by url string @@ -156,12 +173,7 @@ type Provider struct { // Provides returns true if the given scheme is supported by this Provider. func (p Provider) Provides(scheme string) bool { - for _, i := range p.Schemes { - if i == scheme { - return true - } - } - return false + return slices.Contains(p.Schemes, scheme) } // Providers is a collection of Provider objects. @@ -176,7 +188,7 @@ func (p Providers) ByScheme(scheme string) (Getter, error) { return pp.New() } } - return nil, errors.Errorf("scheme %q not supported", scheme) + return nil, fmt.Errorf("scheme %q not supported", scheme) } const ( @@ -188,25 +200,33 @@ const ( var defaultOptions = []Option{WithTimeout(time.Second * DefaultHTTPTimeout)} -var httpProvider = Provider{ - Schemes: []string{"http", "https"}, - New: func(options ...Option) (Getter, error) { - options = append(options, defaultOptions...) - return NewHTTPGetter(options...) - }, -} - -var ociProvider = Provider{ - Schemes: []string{registry.OCIScheme}, - New: NewOCIGetter, +func Getters(extraOpts ...Option) Providers { + return Providers{ + Provider{ + Schemes: []string{"http", "https"}, + New: func(options ...Option) (Getter, error) { + options = append(options, defaultOptions...) + options = append(options, extraOpts...) + return NewHTTPGetter(options...) + }, + }, + Provider{ + Schemes: []string{registry.OCIScheme}, + New: func(options ...Option) (Getter, error) { + options = append(options, defaultOptions...) + options = append(options, extraOpts...) + return NewOCIGetter(options...) + }, + }, + } } // All finds all of the registered getters as a list of Provider instances. // Currently, the built-in getters and the discovered plugins with downloader // notations are collected. -func All(settings *cli.EnvSettings) Providers { - result := Providers{httpProvider, ociProvider} - pluginDownloaders, _ := collectPlugins(settings) +func All(settings *cli.EnvSettings, opts ...Option) Providers { + result := Getters(opts...) + pluginDownloaders, _ := collectGetterPlugins(settings) result = append(result, pluginDownloaders...) return result } diff --git a/pkg/helm/pkg/getter/getter_test.go b/pkg/helm/pkg/getter/getter_test.go index 0ba28bc7..74628968 100644 --- a/pkg/helm/pkg/getter/getter_test.go +++ b/pkg/helm/pkg/getter/getter_test.go @@ -17,12 +17,9 @@ package getter import ( "testing" - - "github.com/werf/nelm/pkg/helm/pkg/cli" + "time" ) -const pluginDir = "testdata/plugins" - func TestProvider(t *testing.T) { p := Provider{ []string{"one", "three"}, @@ -52,29 +49,19 @@ func TestProviders(t *testing.T) { } } -func TestAll(t *testing.T) { - env := cli.New() - env.PluginsDirectory = pluginDir - - all := All(env) - if len(all) != 4 { - t.Errorf("expected 4 providers (default plus three plugins), got %d", len(all)) - } - - if _, err := all.ByScheme("test2"); err != nil { +func TestProvidersWithTimeout(t *testing.T) { + want := time.Hour + getters := Getters(WithTimeout(want)) + getter, err := getters.ByScheme("http") + if err != nil { t.Error(err) } -} - -func TestByScheme(t *testing.T) { - env := cli.New() - env.PluginsDirectory = pluginDir - - g := All(env) - if _, err := g.ByScheme("test"); err != nil { + client, err := getter.(*HTTPGetter).httpClient() + if err != nil { t.Error(err) } - if _, err := g.ByScheme("https"); err != nil { - t.Error(err) + got := client.Timeout + if got != want { + t.Errorf("Expected %q, got %q", want, got) } } diff --git a/pkg/helm/pkg/getter/httpgetter.go b/pkg/helm/pkg/getter/httpgetter.go index de257b2a..059fbfc9 100644 --- a/pkg/helm/pkg/getter/httpgetter.go +++ b/pkg/helm/pkg/getter/httpgetter.go @@ -18,21 +18,19 @@ package getter import ( "bytes" "crypto/tls" + "fmt" "io" "net/http" "net/url" "sync" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/intern/tlsutil" - "github.com/werf/nelm/pkg/helm/intern/urlutil" "github.com/werf/nelm/pkg/helm/intern/version" ) // HTTPGetter is the default HTTP(/S) backend handler type HTTPGetter struct { - opts options + opts getterOptions transport *http.Transport once sync.Once } @@ -53,6 +51,10 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { return nil, err } + if g.opts.acceptHeader != "" { + req.Header.Set("Accept", g.opts.acceptHeader) + } + req.Header.Set("User-Agent", version.GetUserAgent()) if g.opts.userAgent != "" { req.Header.Set("User-Agent", g.opts.userAgent) @@ -62,11 +64,11 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { // with the basic auth is the one being fetched. u1, err := url.Parse(g.opts.url) if err != nil { - return nil, errors.Wrap(err, "Unable to parse getter URL") + return nil, fmt.Errorf("unable to parse getter URL: %w", err) } u2, err := url.Parse(href) if err != nil { - return nil, errors.Wrap(err, "Unable to parse URL getting from") + return nil, fmt.Errorf("unable to parse URL getting from: %w", err) } // Host on URL (returned from url.Parse) contains the port if present. @@ -89,7 +91,7 @@ func (g *HTTPGetter) get(href string) (*bytes.Buffer, error) { } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return nil, errors.Errorf("failed to fetch %s : %s", href, resp.Status) + return nil, fmt.Errorf("failed to fetch %s : %s", href, resp.Status) } buf := bytes.NewBuffer(nil) @@ -120,20 +122,21 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { g.transport = &http.Transport{ DisableCompression: true, Proxy: http.ProxyFromEnvironment, + // Being nil would cause the tls.Config default to be used + // "NewTLSConfig" modifies an empty TLS config, not the default one + TLSClientConfig: &tls.Config{}, } }) if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS { - tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile, g.opts.insecureSkipVerifyTLS) - if err != nil { - return nil, errors.Wrap(err, "can't create TLS config for client") - } - - sni, err := urlutil.ExtractHostname(g.opts.url) + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(g.opts.insecureSkipVerifyTLS), + tlsutil.WithCertKeyPairFiles(g.opts.certFile, g.opts.keyFile), + tlsutil.WithCAFile(g.opts.caFile), + ) if err != nil { - return nil, err + return nil, fmt.Errorf("can't create TLS config for client: %w", err) } - tlsConf.ServerName = sni g.transport.TLSClientConfig = tlsConf } diff --git a/pkg/helm/pkg/getter/httpgetter_test.go b/pkg/helm/pkg/getter/httpgetter_test.go index aa44ea8c..cac1c165 100644 --- a/pkg/helm/pkg/getter/httpgetter_test.go +++ b/pkg/helm/pkg/getter/httpgetter_test.go @@ -28,8 +28,6 @@ import ( "testing" "time" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/intern/tlsutil" "github.com/werf/nelm/pkg/helm/intern/version" "github.com/werf/nelm/pkg/helm/pkg/cli" @@ -52,7 +50,7 @@ func TestHTTPGetter(t *testing.T) { timeout := time.Second * 5 transport := &http.Transport{} - // Test with options + // Test with getterOptions g, err = NewHTTPGetter( WithBasicAuth("I", "Am"), WithPassCredentialsAll(false), @@ -280,17 +278,44 @@ func TestDownload(t *testing.T) { if got.String() != expect { t.Errorf("Expected %q, got %q", expect, got.String()) } + + // test server with varied Accept Header + const expectedAcceptHeader = "application/gzip,application/octet-stream" + acceptHeaderSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Accept") != expectedAcceptHeader { + t.Errorf("Expected '%s', got '%s'", expectedAcceptHeader, r.Header.Get("Accept")) + } + fmt.Fprint(w, expect) + })) + + defer acceptHeaderSrv.Close() + + u, _ = url.ParseRequestURI(acceptHeaderSrv.URL) + httpgetter, err = NewHTTPGetter( + WithAcceptHeader(expectedAcceptHeader), + ) + if err != nil { + t.Fatal(err) + } + _, err = httpgetter.Get(u.String()) + if err != nil { + t.Fatal(err) + } } func TestDownloadTLS(t *testing.T) { cd := "../../testdata" ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") - insecureSkipTLSverify := false + insecureSkipTLSVerify := false - tlsSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca, insecureSkipTLSverify) + tlsSrv := httptest.NewUnstartedServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify), + tlsutil.WithCertKeyPairFiles(pub, priv), + tlsutil.WithCAFile(ca), + ) if err != nil { - t.Fatal(errors.Wrap(err, "can't create TLS config for client")) + t.Fatal(fmt.Errorf("can't create TLS config for client: %w", err)) } tlsConf.ServerName = "helm.sh" tlsSrv.TLS = tlsConf @@ -331,8 +356,133 @@ func TestDownloadTLS(t *testing.T) { } } +func TestDownloadTLSWithRedirect(t *testing.T) { + cd := "../../testdata" + srv2Resp := "hello" + insecureSkipTLSVerify := false + + // Server 2 that will actually fulfil the request. + ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "localhost-crt.pem"), filepath.Join(cd, "key.pem") + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithCAFile(ca), + tlsutil.WithCertKeyPairFiles(pub, priv), + tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify), + ) + + if err != nil { + t.Fatal(fmt.Errorf("can't create TLS config for client: %w", err)) + } + + tlsSrv2 := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, _ *http.Request) { + rw.Header().Set("Content-Type", "text/plain") + rw.Write([]byte(srv2Resp)) + })) + + tlsSrv2.TLS = tlsConf + tlsSrv2.StartTLS() + defer tlsSrv2.Close() + + // Server 1 responds with a redirect to Server 2. + ca, pub, priv = filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + tlsConf, err = tlsutil.NewTLSConfig( + tlsutil.WithCAFile(ca), + tlsutil.WithCertKeyPairFiles(pub, priv), + tlsutil.WithInsecureSkipVerify(insecureSkipTLSVerify), + ) + + if err != nil { + t.Fatal(fmt.Errorf("can't create TLS config for client: %w", err)) + } + + tlsSrv1 := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + u, _ := url.ParseRequestURI(tlsSrv2.URL) + + // Make the request using the hostname 'localhost' (to which 'localhost-crt.pem' is issued) + // to verify that a successful TLS connection is made even if the client doesn't specify + // the hostname (SNI) in `tls.Config.ServerName`. By default the hostname is derived from the + // request URL for every request (including redirects). Setting `tls.Config.ServerName` on the + // client just overrides the remote endpoint's hostname. + // See https://github.com/golang/go/blob/3979fb9/src/net/http/transport.go#L1505-L1513. + u.Host = fmt.Sprintf("localhost:%s", u.Port()) + + http.Redirect(rw, r, u.String(), http.StatusTemporaryRedirect) + })) + + tlsSrv1.TLS = tlsConf + tlsSrv1.StartTLS() + defer tlsSrv1.Close() + + u, _ := url.ParseRequestURI(tlsSrv1.URL) + + t.Run("Test with TLS", func(t *testing.T) { + g, err := NewHTTPGetter( + WithURL(u.String()), + WithTLSClientConfig(pub, priv, ca), + ) + if err != nil { + t.Fatal(err) + } + + buf, err := g.Get(u.String()) + if err != nil { + t.Error(err) + } + + b, err := io.ReadAll(buf) + if err != nil { + t.Error(err) + } + + if string(b) != srv2Resp { + t.Errorf("expected response from Server2 to be '%s', instead got: %s", srv2Resp, string(b)) + } + }) + + t.Run("Test with TLS config being passed along in .Get (see #6635)", func(t *testing.T) { + g, err := NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + buf, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig(pub, priv, ca)) + if err != nil { + t.Error(err) + } + + b, err := io.ReadAll(buf) + if err != nil { + t.Error(err) + } + + if string(b) != srv2Resp { + t.Errorf("expected response from Server2 to be '%s', instead got: %s", srv2Resp, string(b)) + } + }) + + t.Run("Test with only the CA file (see also #6635)", func(t *testing.T) { + g, err := NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + buf, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig("", "", ca)) + if err != nil { + t.Error(err) + } + + b, err := io.ReadAll(buf) + if err != nil { + t.Error(err) + } + + if string(b) != srv2Resp { + t.Errorf("expected response from Server2 to be '%s', instead got: %s", srv2Resp, string(b)) + } + }) +} + func TestDownloadInsecureSkipTLSVerify(t *testing.T) { - ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) defer ts.Close() u, _ := url.ParseRequestURI(ts.URL) @@ -364,7 +514,7 @@ func TestDownloadInsecureSkipTLSVerify(t *testing.T) { } func TestHTTPGetterTarDownload(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { f, _ := os.Open("testdata/empty-0.0.1.tgz") defer f.Close() @@ -423,12 +573,10 @@ func TestHttpClientInsecureSkipVerify(t *testing.T) { if len(transport.TLSClientConfig.Certificates) <= 0 { t.Fatal("transport.TLSClientConfig.Certificates is not present") } - if transport.TLSClientConfig.ServerName == "" { - t.Fatal("TLSClientConfig.ServerName is blank") - } } func verifyInsecureSkipVerify(t *testing.T, g *HTTPGetter, caseName string, expectedValue bool) *http.Transport { + t.Helper() returnVal, err := g.httpClient() if err != nil { diff --git a/pkg/helm/pkg/getter/ocigetter.go b/pkg/helm/pkg/getter/ocigetter.go index b2ea0303..65bc6d26 100644 --- a/pkg/helm/pkg/getter/ocigetter.go +++ b/pkg/helm/pkg/getter/ocigetter.go @@ -17,9 +17,11 @@ package getter import ( "bytes" + "crypto/tls" "fmt" "net" "net/http" + "path" "strings" "sync" "time" @@ -31,7 +33,7 @@ import ( // OCIGetter is the default HTTP(/S) backend handler type OCIGetter struct { - opts options + opts getterOptions transport *http.Transport once sync.Once } @@ -58,6 +60,15 @@ func (g *OCIGetter) get(href string) (*bytes.Buffer, error) { ref := strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)) + if version := g.opts.version; version != "" && !strings.Contains(path.Base(ref), ":") { + ref = fmt.Sprintf("%s:%s", ref, version) + } + // Check if this is a plugin request + if g.opts.artifactType == "plugin" { + return g.getPlugin(client, ref) + } + + // Default to chart behavior for backward compatibility var pullOpts []registry.PullOption requestingProv := strings.HasSuffix(ref, ".prov") if requestingProv { @@ -119,11 +130,19 @@ func (g *OCIGetter) newRegistryClient() (*registry.Client, error) { IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, + Proxy: http.ProxyFromEnvironment, + // Being nil would cause the tls.Config default to be used + // "NewTLSConfig" modifies an empty TLS config, not the default one + TLSClientConfig: &tls.Config{}, } }) if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" || g.opts.insecureSkipVerifyTLS { - tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile, g.opts.insecureSkipVerifyTLS) + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(g.opts.insecureSkipVerifyTLS), + tlsutil.WithCertKeyPairFiles(g.opts.certFile, g.opts.keyFile), + tlsutil.WithCAFile(g.opts.caFile), + ) if err != nil { return nil, fmt.Errorf("can't create TLS config for client: %w", err) } @@ -153,3 +172,42 @@ func (g *OCIGetter) newRegistryClient() (*registry.Client, error) { return client, nil } + +// getPlugin handles plugin-specific OCI pulls +func (g *OCIGetter) getPlugin(client *registry.Client, ref string) (*bytes.Buffer, error) { + // Check if this is a provenance file request + requestingProv := strings.HasSuffix(ref, ".prov") + if requestingProv { + ref = strings.TrimSuffix(ref, ".prov") + } + + // Extract plugin name from the reference + // e.g., "ghcr.io/user/plugin-name:v1.0.0" -> "plugin-name" + parts := strings.Split(ref, "/") + if len(parts) < 2 { + return nil, fmt.Errorf("invalid OCI reference: %s", ref) + } + lastPart := parts[len(parts)-1] + pluginName := lastPart + if idx := strings.LastIndex(lastPart, ":"); idx > 0 { + pluginName = lastPart[:idx] + } + if idx := strings.LastIndex(lastPart, "@"); idx > 0 { + pluginName = lastPart[:idx] + } + + var pullOpts []registry.PluginPullOption + if requestingProv { + pullOpts = append(pullOpts, registry.PullPluginOptWithProv(true)) + } + + result, err := client.PullPlugin(ref, pluginName, pullOpts...) + if err != nil { + return nil, err + } + + if requestingProv { + return bytes.NewBuffer(result.Prov.Data), nil + } + return bytes.NewBuffer(result.PluginData), nil +} diff --git a/pkg/helm/pkg/getter/ocigetter_test.go b/pkg/helm/pkg/getter/ocigetter_test.go index 983ace40..ec79ea55 100644 --- a/pkg/helm/pkg/getter/ocigetter_test.go +++ b/pkg/helm/pkg/getter/ocigetter_test.go @@ -42,7 +42,7 @@ func TestOCIGetter(t *testing.T) { insecureSkipVerifyTLS := false plainHTTP := false - // Test with options + // Test with getterOptions g, err = NewOCIGetter( WithBasicAuth("I", "Am"), WithTLSClientConfig(pub, priv, ca), diff --git a/pkg/helm/pkg/getter/plugingetter.go b/pkg/helm/pkg/getter/plugingetter.go index ce1d990a..4e8b723f 100644 --- a/pkg/helm/pkg/getter/plugingetter.go +++ b/pkg/helm/pkg/getter/plugingetter.go @@ -16,95 +16,11 @@ limitations under the License. package getter import ( - "bytes" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/cli" - "github.com/werf/nelm/pkg/helm/pkg/plugin" ) -// collectPlugins scans for getter plugins. +// collectGetterPlugins scans for getter plugins. // This will load plugins according to the cli. -func collectPlugins(settings *cli.EnvSettings) (Providers, error) { - plugins, err := plugin.FindPlugins(settings.PluginsDirectory) - if err != nil { - return nil, err - } - var result Providers - for _, plugin := range plugins { - for _, downloader := range plugin.Metadata.Downloaders { - result = append(result, Provider{ - Schemes: downloader.Protocols, - New: NewPluginGetter( - downloader.Command, - settings, - plugin.Metadata.Name, - plugin.Dir, - ), - }) - } - } - return result, nil -} - -// pluginGetter is a generic type to invoke custom downloaders, -// implemented in plugins. -type pluginGetter struct { - command string - settings *cli.EnvSettings - name string - base string - opts options -} - -func (p *pluginGetter) setupOptionsEnv(env []string) []string { - env = append(env, fmt.Sprintf("HELM_PLUGIN_USERNAME=%s", p.opts.username)) - env = append(env, fmt.Sprintf("HELM_PLUGIN_PASSWORD=%s", p.opts.password)) - env = append(env, fmt.Sprintf("HELM_PLUGIN_PASS_CREDENTIALS_ALL=%t", p.opts.passCredentialsAll)) - return env -} - -// Get runs downloader plugin command -func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error) { - for _, opt := range options { - opt(&p.opts) - } - commands := strings.Split(p.command, " ") - argv := append(commands[1:], p.opts.certFile, p.opts.keyFile, p.opts.caFile, href) - prog := exec.Command(filepath.Join(p.base, commands[0]), argv...) - plugin.SetupPluginEnv(p.settings, p.name, p.base) - prog.Env = p.setupOptionsEnv(os.Environ()) - buf := bytes.NewBuffer(nil) - prog.Stdout = buf - prog.Stderr = os.Stderr - if err := prog.Run(); err != nil { - if eerr, ok := err.(*exec.ExitError); ok { - os.Stderr.Write(eerr.Stderr) - return nil, errors.Errorf("plugin %q exited with error", p.command) - } - return nil, err - } - return buf, nil -} - -// NewPluginGetter constructs a valid plugin getter -func NewPluginGetter(command string, settings *cli.EnvSettings, name, base string) Constructor { - return func(options ...Option) (Getter, error) { - result := &pluginGetter{ - command: command, - settings: settings, - name: name, - base: base, - } - for _, opt := range options { - opt(&result.opts) - } - return result, nil - } +func collectGetterPlugins(_ *cli.EnvSettings) (Providers, error) { + return nil, nil } diff --git a/pkg/helm/pkg/getter/plugingetter_test.go b/pkg/helm/pkg/getter/plugingetter_test.go deleted file mode 100644 index 84e7009f..00000000 --- a/pkg/helm/pkg/getter/plugingetter_test.go +++ /dev/null @@ -1,101 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package getter - -import ( - "runtime" - "strings" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/cli" -) - -func TestCollectPlugins(t *testing.T) { - env := cli.New() - env.PluginsDirectory = pluginDir - - p, err := collectPlugins(env) - if err != nil { - t.Fatal(err) - } - - if len(p) != 2 { - t.Errorf("Expected 2 plugins, got %d: %v", len(p), p) - } - - if _, err := p.ByScheme("test2"); err != nil { - t.Error(err) - } - - if _, err := p.ByScheme("test"); err != nil { - t.Error(err) - } - - if _, err := p.ByScheme("nosuchthing"); err == nil { - t.Fatal("did not expect protocol handler for nosuchthing") - } -} - -func TestPluginGetter(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("TODO: refactor this test to work on windows") - } - - env := cli.New() - env.PluginsDirectory = pluginDir - pg := NewPluginGetter("echo", env, "test", ".") - g, err := pg() - if err != nil { - t.Fatal(err) - } - - data, err := g.Get("test://foo/bar") - if err != nil { - t.Fatal(err) - } - - expect := "test://foo/bar" - got := strings.TrimSpace(data.String()) - if got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } -} - -func TestPluginSubCommands(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("TODO: refactor this test to work on windows") - } - - env := cli.New() - env.PluginsDirectory = pluginDir - - pg := NewPluginGetter("echo -n", env, "test", ".") - g, err := pg() - if err != nil { - t.Fatal(err) - } - - data, err := g.Get("test://foo/bar") - if err != nil { - t.Fatal(err) - } - - expect := " test://foo/bar" - got := data.String() - if got != expect { - t.Errorf("Expected %q, got %q", expect, got) - } -} diff --git a/pkg/helm/pkg/getter/testdata/plugins/testgetter/get.sh b/pkg/helm/pkg/getter/testdata/plugins/testgetter/get.sh deleted file mode 100755 index cdd99236..00000000 --- a/pkg/helm/pkg/getter/testdata/plugins/testgetter/get.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo ENVIRONMENT -env - -echo "" -echo ARGUMENTS -echo $@ diff --git a/pkg/helm/pkg/getter/testdata/plugins/testgetter/plugin.yaml b/pkg/helm/pkg/getter/testdata/plugins/testgetter/plugin.yaml deleted file mode 100644 index d1b929e3..00000000 --- a/pkg/helm/pkg/getter/testdata/plugins/testgetter/plugin.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: "testgetter" -version: "0.1.0" -usage: "Fetch a package from a test:// source" -description: |- - Print the environment that the plugin was given, then exit. - - This registers the test:// protocol. - -command: "$HELM_PLUGIN_DIR/get.sh" -ignoreFlags: true -downloaders: -#- command: "$HELM_PLUGIN_DIR/get.sh" -- command: "echo" - protocols: - - "test" diff --git a/pkg/helm/pkg/getter/testdata/plugins/testgetter2/get.sh b/pkg/helm/pkg/getter/testdata/plugins/testgetter2/get.sh deleted file mode 100755 index cdd99236..00000000 --- a/pkg/helm/pkg/getter/testdata/plugins/testgetter2/get.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -echo ENVIRONMENT -env - -echo "" -echo ARGUMENTS -echo $@ diff --git a/pkg/helm/pkg/getter/testdata/plugins/testgetter2/plugin.yaml b/pkg/helm/pkg/getter/testdata/plugins/testgetter2/plugin.yaml deleted file mode 100644 index f1a527ef..00000000 --- a/pkg/helm/pkg/getter/testdata/plugins/testgetter2/plugin.yaml +++ /dev/null @@ -1,10 +0,0 @@ -name: "testgetter2" -version: "0.1.0" -usage: "Fetch a different package from a test2:// source" -description: "Handle test2 scheme" -command: "$HELM_PLUGIN_DIR/get.sh" -ignoreFlags: true -downloaders: -- command: "echo" - protocols: - - "test2" diff --git a/pkg/helm/pkg/helmpath/home_unix_test.go b/pkg/helm/pkg/helmpath/home_unix_test.go index de8beabc..87748759 100644 --- a/pkg/helm/pkg/helmpath/home_unix_test.go +++ b/pkg/helm/pkg/helmpath/home_unix_test.go @@ -16,7 +16,6 @@ package helmpath import ( - "os" "runtime" "testing" @@ -24,9 +23,9 @@ import ( ) func TestHelmHome(t *testing.T) { - os.Setenv(xdg.CacheHomeEnvVar, "/cache") - os.Setenv(xdg.ConfigHomeEnvVar, "/config") - os.Setenv(xdg.DataHomeEnvVar, "/data") + t.Setenv(xdg.CacheHomeEnvVar, "/cache") + t.Setenv(xdg.ConfigHomeEnvVar, "/config") + t.Setenv(xdg.DataHomeEnvVar, "/data") isEq := func(t *testing.T, got, expected string) { t.Helper() if expected != got { @@ -40,7 +39,7 @@ func TestHelmHome(t *testing.T) { isEq(t, DataPath(), "/data/helm") // test to see if lazy-loading environment variables at runtime works - os.Setenv(xdg.CacheHomeEnvVar, "/cache2") + t.Setenv(xdg.CacheHomeEnvVar, "/cache2") isEq(t, CachePath(), "/cache2/helm") } diff --git a/pkg/helm/pkg/helmpath/lazypath.go b/pkg/helm/pkg/helmpath/lazypath.go index 09e58dc2..b71ebb3e 100644 --- a/pkg/helm/pkg/helmpath/lazypath.go +++ b/pkg/helm/pkg/helmpath/lazypath.go @@ -34,20 +34,11 @@ const ( DataHomeEnvVar = "HELM_DATA_HOME" ) -// lazypath is an lazy-loaded path buffer for the XDG base directory specification. +// lazypath is a lazy-loaded path buffer for the XDG base directory specification. type lazypath string -func (l lazypath) path(helmEnvVar, xdgEnvVar string, defaultFn func() string, elem ...string) string { - - // There is an order to checking for a path. - // 1. See if a Helm specific environment variable has been set. - // 2. Check if an XDG environment variable is set - // 3. Fall back to a default - base := os.Getenv(helmEnvVar) - if base != "" { - return filepath.Join(base, filepath.Join(elem...)) - } - base = os.Getenv(xdgEnvVar) +func (l lazypath) path(_, xdgEnvVar string, defaultFn func() string, elem ...string) string { + base := os.Getenv(xdgEnvVar) if base == "" { base = defaultFn() } diff --git a/pkg/helm/pkg/helmpath/lazypath_darwin_test.go b/pkg/helm/pkg/helmpath/lazypath_darwin_test.go index ec49ed53..4de08f0d 100644 --- a/pkg/helm/pkg/helmpath/lazypath_darwin_test.go +++ b/pkg/helm/pkg/helmpath/lazypath_darwin_test.go @@ -40,7 +40,7 @@ func TestDataPath(t *testing.T) { t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) } - os.Setenv(xdg.DataHomeEnvVar, "/tmp") + t.Setenv(xdg.DataHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) @@ -58,7 +58,7 @@ func TestConfigPath(t *testing.T) { t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) } - os.Setenv(xdg.ConfigHomeEnvVar, "/tmp") + t.Setenv(xdg.ConfigHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) @@ -76,7 +76,7 @@ func TestCachePath(t *testing.T) { t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) } - os.Setenv(xdg.CacheHomeEnvVar, "/tmp") + t.Setenv(xdg.CacheHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) diff --git a/pkg/helm/pkg/helmpath/lazypath_unix_test.go b/pkg/helm/pkg/helmpath/lazypath_unix_test.go index 87bf9c31..1bb3c2ff 100644 --- a/pkg/helm/pkg/helmpath/lazypath_unix_test.go +++ b/pkg/helm/pkg/helmpath/lazypath_unix_test.go @@ -16,7 +16,6 @@ package helmpath import ( - "os" "path/filepath" "testing" @@ -32,15 +31,13 @@ const ( ) func TestDataPath(t *testing.T) { - os.Unsetenv(xdg.DataHomeEnvVar) - expected := filepath.Join(homedir.HomeDir(), ".local", "share", appName, testFile) if lazy.dataPath(testFile) != expected { t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) } - os.Setenv(xdg.DataHomeEnvVar, "/tmp") + t.Setenv(xdg.DataHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) @@ -50,15 +47,13 @@ func TestDataPath(t *testing.T) { } func TestConfigPath(t *testing.T) { - os.Unsetenv(xdg.ConfigHomeEnvVar) - expected := filepath.Join(homedir.HomeDir(), ".config", appName, testFile) if lazy.configPath(testFile) != expected { t.Errorf("expected '%s', got '%s'", expected, lazy.configPath(testFile)) } - os.Setenv(xdg.ConfigHomeEnvVar, "/tmp") + t.Setenv(xdg.ConfigHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) @@ -68,15 +63,13 @@ func TestConfigPath(t *testing.T) { } func TestCachePath(t *testing.T) { - os.Unsetenv(xdg.CacheHomeEnvVar) - expected := filepath.Join(homedir.HomeDir(), ".cache", appName, testFile) if lazy.cachePath(testFile) != expected { t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) } - os.Setenv(xdg.CacheHomeEnvVar, "/tmp") + t.Setenv(xdg.CacheHomeEnvVar, "/tmp") expected = filepath.Join("/tmp", appName, testFile) diff --git a/pkg/helm/pkg/ignore/doc.go b/pkg/helm/pkg/ignore/doc.go index 5245d410..f07764ea 100644 --- a/pkg/helm/pkg/ignore/doc.go +++ b/pkg/helm/pkg/ignore/doc.go @@ -26,7 +26,7 @@ The formatting rules are as follows: - Parsing is line-by-line - Empty lines are ignored - - Lines the begin with # (comments) will be ignored + - Lines that begin with # (comments) will be ignored - Leading and trailing spaces are always ignored - Inline comments are NOT supported ('foo* # Any foo' does not contain a comment) - There is no support for multi-line patterns @@ -65,4 +65,4 @@ Notable differences from .gitignore: - The evaluation of escape sequences has not been tested for compatibility - There is no support for '\!' as a special leading sequence. */ -package ignore // import "helm.sh/helm/v3/pkg/ignore" +package ignore // import "github.com/werf/nelm/pkg/helm/pkg/ignore" diff --git a/pkg/helm/pkg/ignore/rules.go b/pkg/helm/pkg/ignore/rules.go index a80923ba..a8160da2 100644 --- a/pkg/helm/pkg/ignore/rules.go +++ b/pkg/helm/pkg/ignore/rules.go @@ -19,13 +19,12 @@ package ignore import ( "bufio" "bytes" + "errors" "io" - "log" + "log/slog" "os" "path/filepath" "strings" - - "github.com/pkg/errors" ) // HelmIgnore default name of an ignorefile. @@ -102,7 +101,7 @@ func (r *Rules) Ignore(path string, fi os.FileInfo) bool { } for _, p := range r.patterns { if p.match == nil { - log.Printf("ignore: no matcher supplied for %q", p.raw) + slog.Info("this will be ignored no matcher supplied", "patterns", p.raw) return false } @@ -171,35 +170,39 @@ func (r *Rules) parseRule(rule string) error { rule = strings.TrimSuffix(rule, "/") } - if strings.HasPrefix(rule, "/") { + if after, ok := strings.CutPrefix(rule, "/"); ok { // Require path matches the root path. - p.match = func(n string, fi os.FileInfo) bool { - rule = strings.TrimPrefix(rule, "/") + p.match = func(n string, _ os.FileInfo) bool { + rule = after ok, err := filepath.Match(rule, n) if err != nil { - log.Printf("Failed to compile %q: %s", rule, err) + slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err)) return false } return ok } } else if strings.Contains(rule, "/") { // require structural match. - p.match = func(n string, fi os.FileInfo) bool { + p.match = func(n string, _ os.FileInfo) bool { ok, err := filepath.Match(rule, n) if err != nil { - log.Printf("Failed to compile %q: %s", rule, err) + slog.Error( + "failed to compile", + slog.String("rule", rule), + slog.Any("error", err), + ) return false } return ok } } else { - p.match = func(n string, fi os.FileInfo) bool { + p.match = func(n string, _ os.FileInfo) bool { // When there is no slash in the pattern, we evaluate ONLY the // filename. n = filepath.Base(n) ok, err := filepath.Match(rule, n) if err != nil { - log.Printf("Failed to compile %q: %s", rule, err) + slog.Error("failed to compile", slog.String("rule", rule), slog.Any("error", err)) return false } return ok diff --git a/pkg/helm/pkg/kube/client.go b/pkg/helm/pkg/kube/client.go index 3b2017fb..0e07043c 100644 --- a/pkg/helm/pkg/kube/client.go +++ b/pkg/helm/pkg/kube/client.go @@ -14,49 +14,50 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" + "log/slog" + "net/http" "os" "path/filepath" "reflect" "strings" "sync" - "time" - jsonpatch "github.com/evanphx/json-patch" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" - batch "k8s.io/api/batch/v1" + jsonpatch "github.com/evanphx/json-patch/v5" v1 "k8s.io/api/core/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + + "github.com/werf/nelm/pkg/helm/intern/logging" - multierror "github.com/hashicorp/go-multierror" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" - "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/jsonmergepatch" + "k8s.io/apimachinery/pkg/util/mergepatch" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/strategicpatch" - "k8s.io/apimachinery/pkg/watch" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" - cachetools "k8s.io/client-go/tools/cache" - watchtools "k8s.io/client-go/tools/watch" + "k8s.io/client-go/util/csaupgrade" + "k8s.io/client-go/util/retry" cmdutil "k8s.io/kubectl/pkg/cmd/util" ) @@ -67,7 +68,7 @@ var metadataAccessor = meta.NewAccessor() // ManagedFieldsManager is the name of the manager of Kubernetes managedFields // first introduced in Kubernetes 1.18 -var ManagedFieldsManager string +var ManagedFieldsManager = "helm" // Client represents a client capable of communicating with the Kubernetes API. type Client struct { @@ -80,43 +81,160 @@ type Client struct { // needs. The smaller surface area of the interface means there is a lower // chance of it changing. Factory Factory - Log func(string, ...interface{}) // Namespace allows to bypass the kubeconfig file for the choice of the namespace Namespace string - kubeClient *kubernetes.Clientset + // WaitContext is an optional context to use for wait operations. + // If not set, a context will be created internally using the + // timeout provided to the wait functions. + // + // Deprecated: Use WithWaitContext wait option when getting a Waiter instead. + WaitContext context.Context + + Waiter + kubeClient kubernetes.Interface + + // Embed a LogHolder to provide logger functionality + logging.LogHolder +} + +var _ Interface = (*Client)(nil) + +// WaitStrategy represents the algorithm used to wait for Kubernetes +// resources to reach their desired state. +type WaitStrategy string + +const ( + // StatusWatcherStrategy: event-driven waits using kstatus (watches + aggregated readers). + // Default for --wait. More accurate and responsive; waits CRs and full reconciliation. + // Requires: reachable API server, list+watch RBAC on deployed resources, and a non-zero timeout. + StatusWatcherStrategy WaitStrategy = "watcher" + + // LegacyStrategy: Helm 3-style periodic polling until ready or timeout. + // Use when watches aren’t available/reliable, or for compatibility/simple CI. + // Requires only list RBAC for polled resources. + LegacyStrategy WaitStrategy = "legacy" + + // HookOnlyStrategy: wait only for hook Pods/Jobs to complete; does not wait for general chart resources. + HookOnlyStrategy WaitStrategy = "hookOnly" +) + +type FieldValidationDirective string + +const ( + FieldValidationDirectiveIgnore FieldValidationDirective = "Ignore" + FieldValidationDirectiveWarn FieldValidationDirective = "Warn" + FieldValidationDirectiveStrict FieldValidationDirective = "Strict" +) + +type CreateApplyFunc func(target *resource.Info) error +type UpdateApplyFunc func(original, target *resource.Info) error + +func init() { + // Add CRDs to the scheme. They are missing by default. + if err := apiextv1.AddToScheme(scheme.Scheme); err != nil { + // This should never happen. + panic(err) + } + if err := apiextv1beta1.AddToScheme(scheme.Scheme); err != nil { + panic(err) + } +} + +func (c *Client) newStatusWatcher(opts ...WaitOption) (*statusWaiter, error) { + var o waitOptions + for _, opt := range opts { + opt(&o) + } + cfg, err := c.Factory.ToRESTConfig() + if err != nil { + return nil, err + } + dynamicClient, err := c.Factory.DynamicClient() + if err != nil { + return nil, err + } + httpClient, err := rest.HTTPClientFor(cfg) + if err != nil { + return nil, err + } + restMapper, err := apiutil.NewDynamicRESTMapper(cfg, httpClient) + if err != nil { + return nil, err + } + waitContext := o.ctx + if waitContext == nil { + waitContext = c.WaitContext + } + sw := &statusWaiter{ + restMapper: restMapper, + client: dynamicClient, + ctx: waitContext, + watchUntilReadyCtx: o.watchUntilReadyCtx, + waitCtx: o.waitCtx, + waitWithJobsCtx: o.waitWithJobsCtx, + waitForDeleteCtx: o.waitForDeleteCtx, + readers: o.statusReaders, + } + sw.SetLogger(c.Logger().Handler()) + return sw, nil +} + +func (c *Client) GetWaiter(ws WaitStrategy) (Waiter, error) { + return c.GetWaiterWithOptions(ws) +} - ResourcesWaiter ResourcesWaiter - Extender ClientExtender +func (c *Client) GetWaiterWithOptions(strategy WaitStrategy, opts ...WaitOption) (Waiter, error) { + switch strategy { + case LegacyStrategy: + kc, err := c.Factory.KubernetesClientSet() + if err != nil { + return nil, err + } + return &legacyWaiter{kubeClient: kc, ctx: c.WaitContext}, nil + case StatusWatcherStrategy: + return c.newStatusWatcher(opts...) + case HookOnlyStrategy: + sw, err := c.newStatusWatcher(opts...) + if err != nil { + return nil, err + } + return &hookOnlyWaiter{sw: sw}, nil + case "": + return nil, errors.New("wait strategy not set. Choose one of: " + string(StatusWatcherStrategy) + ", " + string(HookOnlyStrategy) + ", " + string(LegacyStrategy)) + default: + return nil, errors.New("unknown wait strategy (s" + string(strategy) + "). Valid values are: " + string(StatusWatcherStrategy) + ", " + string(HookOnlyStrategy) + ", " + string(LegacyStrategy)) + } +} + +func (c *Client) SetWaiter(ws WaitStrategy) error { + return c.SetWaiterWithOptions(ws) } -var addToScheme sync.Once +func (c *Client) SetWaiterWithOptions(ws WaitStrategy, opts ...WaitOption) error { + var err error + c.Waiter, err = c.GetWaiterWithOptions(ws, opts...) + if err != nil { + return err + } + return nil +} // New creates a new Client. func New(getter genericclioptions.RESTClientGetter) *Client { if getter == nil { getter = genericclioptions.NewConfigFlags(true) } - // Add CRDs to the scheme. They are missing by default. - addToScheme.Do(func() { - if err := apiextv1.AddToScheme(scheme.Scheme); err != nil { - // This should never happen. - panic(err) - } - if err := apiextv1beta1.AddToScheme(scheme.Scheme); err != nil { - panic(err) - } - }) - return &Client{ - Factory: cmdutil.NewFactory(getter), - Log: nopLogger, + factory := cmdutil.NewFactory(getter) + c := &Client{ + Factory: factory, } + c.SetLogger(slog.Default().Handler()) + return c } -var nopLogger = func(_ string, _ ...interface{}) {} - // getKubeClient get or create a new KubernetesClientSet -func (c *Client) getKubeClient() (*kubernetes.Clientset, error) { +func (c *Client) getKubeClient() (kubernetes.Interface, error) { var err error if c.kubeClient == nil { c.kubeClient, err = c.Factory.KubernetesClientSet() @@ -129,37 +247,124 @@ func (c *Client) getKubeClient() (*kubernetes.Clientset, error) { func (c *Client) IsReachable() error { client, err := c.getKubeClient() if err == genericclioptions.ErrEmptyConfig { - // re-replace kubernetes ErrEmptyConfig error with a friendy error + // re-replace kubernetes ErrEmptyConfig error with a friendly error // moar workarounds for Kubernetes API breaking. - return errors.New("Kubernetes cluster unreachable") + return errors.New("kubernetes cluster unreachable") } if err != nil { - return errors.Wrap(err, "Kubernetes cluster unreachable") + return fmt.Errorf("kubernetes cluster unreachable: %w", err) } - if _, err := client.ServerVersion(); err != nil { - return errors.Wrap(err, "Kubernetes cluster unreachable") + if _, err := client.Discovery().ServerVersion(); err != nil { + return fmt.Errorf("kubernetes cluster unreachable: %w", err) } return nil } -// Create creates Kubernetes resources specified in the resource list. -func (c *Client) Create(resources ResourceList, opts CreateOptions) (*Result, error) { - if c.Extender != nil { - if err := perform(resources, c.Extender.BeforeCreateResource); err != nil { - return &Result{}, err +type clientCreateOptions struct { + serverSideApply bool + forceConflicts bool + dryRun bool + fieldValidationDirective FieldValidationDirective +} + +type ClientCreateOption func(*clientCreateOptions) error + +// ClientCreateOptionServerSideApply enables performing object apply server-side +// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/ +// +// `forceConflicts` forces conflicts to be resolved (may be when serverSideApply enabled only) +// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts +func ClientCreateOptionServerSideApply(serverSideApply, forceConflicts bool) ClientCreateOption { + return func(o *clientCreateOptions) error { + if !serverSideApply && forceConflicts { + return fmt.Errorf("forceConflicts enabled when serverSideApply disabled") } + + o.serverSideApply = serverSideApply + o.forceConflicts = forceConflicts + + return nil } +} - c.Log("creating %d resource(s)", len(resources)) +// ClientCreateOptionDryRun requests the server to perform non-mutating operations only +func ClientCreateOptionDryRun(dryRun bool) ClientCreateOption { + return func(o *clientCreateOptions) error { + o.dryRun = dryRun - var fn func(*resource.Info) (performResourceStatus, error) - if opts.SkipIfAlreadyExists { - fn = createResourceSkipIfExists - } else { - fn = createResource + return nil } +} - return performWithResult(resources, fn) +// ClientCreateOptionFieldValidationDirective specifies how API operations validate object's schema +// - For client-side apply: this is ignored +// - For server-side apply: the directive is sent to the server to perform the validation +// +// Defaults to `FieldValidationDirectiveStrict` +func ClientCreateOptionFieldValidationDirective(fieldValidationDirective FieldValidationDirective) ClientCreateOption { + return func(o *clientCreateOptions) error { + o.fieldValidationDirective = fieldValidationDirective + + return nil + } +} + +func (c *Client) makeCreateApplyFunc(serverSideApply, forceConflicts, dryRun bool, fieldValidationDirective FieldValidationDirective) CreateApplyFunc { + if serverSideApply { + c.Logger().Debug( + "using server-side apply for resource creation", + slog.Bool("forceConflicts", forceConflicts), + slog.Bool("dryRun", dryRun), + slog.String("fieldValidationDirective", string(fieldValidationDirective))) + + return func(target *resource.Info) error { + err := patchResourceServerSide(target, dryRun, forceConflicts, fieldValidationDirective) + + logger := c.Logger().With( + slog.String("namespace", target.Namespace), + slog.String("name", target.Name), + slog.String("gvk", target.Mapping.GroupVersionKind.String())) + if err != nil { + logger.Debug("Error creating resource via patch", slog.Any("error", err)) + return err + } + + logger.Debug("Created resource via patch") + + return nil + } + } + + c.Logger().Debug("using client-side apply for resource creation") + return createResource +} + +// Create creates Kubernetes resources specified in the resource list. +func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) { + c.Logger().Debug("creating resource(s)", "resources", len(resources)) + + createOptions := clientCreateOptions{ + serverSideApply: true, // Default to server-side apply + fieldValidationDirective: FieldValidationDirectiveStrict, + } + + errs := make([]error, 0, len(options)) + for _, o := range options { + errs = append(errs, o(&createOptions)) + } + if err := errors.Join(errs...); err != nil { + return nil, fmt.Errorf("invalid client create option(s): %w", err) + } + + createApplyFunc := c.makeCreateApplyFunc( + createOptions.serverSideApply, + createOptions.forceConflicts, + createOptions.dryRun, + createOptions.fieldValidationDirective) + if err := perform(resources, createApplyFunc); err != nil { + return nil, err + } + return &Result{Created: resources}, nil } func transformRequests(req *rest.Request) { @@ -207,7 +412,7 @@ func (c *Client) Get(resources ResourceList, related bool) (map[string][]runtime objs, err = c.getSelectRelationPod(info, objs, isTable, &podSelectors) if err != nil { - c.Log("Warning: get the relation pod is failed, err:%s", err.Error()) + c.Logger().Warn("get the relation pod is failed", slog.Any("error", err)) } } } @@ -225,7 +430,7 @@ func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]run if info == nil { return objs, nil } - c.Log("get relation pod of object: %s/%s/%s", info.Namespace, info.Mapping.GroupVersionKind.Kind, info.Name) + c.Logger().Debug("get relation pod of object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) selector, ok, _ := getSelectorFromObject(info.Object) if !ok { return objs, nil @@ -297,45 +502,6 @@ func getResource(info *resource.Info) (runtime.Object, error) { return obj, nil } -// Wait waits up to the given timeout for the specified resources to be ready. -func (c *Client) Wait(resources ResourceList, timeout time.Duration) error { - cs, err := c.getKubeClient() - if err != nil { - return err - } - checker := NewReadyChecker(cs, c.Log, PausedAsReady(true)) - w := waiter{ - c: checker, - log: c.Log, - timeout: timeout, - } - return w.waitForResources(resources) -} - -// WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. -func (c *Client) WaitWithJobs(resources ResourceList, timeout time.Duration) error { - cs, err := c.getKubeClient() - if err != nil { - return err - } - checker := NewReadyChecker(cs, c.Log, PausedAsReady(true), CheckJobs(true)) - w := waiter{ - c: checker, - log: c.Log, - timeout: timeout, - } - return w.waitForResources(resources) -} - -// WaitForDelete wait up to the given timeout for the specified resources to be deleted. -func (c *Client) WaitForDelete(resources ResourceList, timeout time.Duration) error { - w := waiter{ - log: c.Log, - timeout: timeout, - } - return w.waitForDeletedResources(resources) -} - func (c *Client) namespace() string { if c.Namespace != "" { return c.Namespace @@ -346,113 +512,103 @@ func (c *Client) namespace() string { return v1.NamespaceDefault } -// newBuilder returns a new resource builder for structured api objects. -func (c *Client) newBuilder() *resource.Builder { - return c.Factory.NewBuilder(). - ContinueOnError(). - NamespaceParam(c.namespace()). - DefaultNamespace(). - Flatten() -} - -// Build validates for Kubernetes objects and returns unstructured infos. -func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) { - validationDirective := metav1.FieldValidationIgnore +func determineFieldValidationDirective(validate bool) FieldValidationDirective { if validate { - validationDirective = metav1.FieldValidationStrict + return FieldValidationDirectiveStrict } - schema, err := c.Factory.Validator(validationDirective) + return FieldValidationDirectiveIgnore +} + +func buildResourceList(f Factory, namespace string, validationDirective FieldValidationDirective, reader io.Reader, transformRequest resource.RequestTransform) (ResourceList, error) { + + schema, err := f.Validator(string(validationDirective)) if err != nil { return nil, err } - result, err := c.newBuilder(). + + builder := f.NewBuilder(). + ContinueOnError(). + NamespaceParam(namespace). + DefaultNamespace(). + Flatten(). Unstructured(). Schema(schema). - Stream(reader, ""). - Do().Infos() + Stream(reader, "") + if transformRequest != nil { + builder.TransformRequests(transformRequest) + } + result, err := builder.Do().Infos() return result, scrubValidationError(err) } +// Build validates for Kubernetes objects and returns unstructured infos. +func (c *Client) Build(reader io.Reader, validate bool) (ResourceList, error) { + return buildResourceList( + c.Factory, + c.namespace(), + determineFieldValidationDirective(validate), + reader, + nil) +} + // BuildTable validates for Kubernetes objects and returns unstructured infos. // The returned kind is a Table. func (c *Client) BuildTable(reader io.Reader, validate bool) (ResourceList, error) { - validationDirective := metav1.FieldValidationIgnore - if validate { - validationDirective = metav1.FieldValidationStrict - } - - schema, err := c.Factory.Validator(validationDirective) - if err != nil { - return nil, err - } - result, err := c.newBuilder(). - Unstructured(). - Schema(schema). - Stream(reader, ""). - TransformRequests(transformRequests). - Do().Infos() - return result, scrubValidationError(err) + return buildResourceList( + c.Factory, + c.namespace(), + determineFieldValidationDirective(validate), + reader, + transformRequests) } -// Update takes the current list of objects and target list of objects and -// creates resources that don't already exist, updates resources that have been -// modified in the target configuration, and deletes resources from the current -// configuration that are not present in the target configuration. If an error -// occurs, a Result will still be returned with the error, containing all -// resource updates, creations, and deletions that were attempted. These can be -// used for cleanup or other logging purposes. -func (c *Client) Update(original, target ResourceList, force bool, opts UpdateOptions) (*Result, error) { - updateErrors := []string{} +func (c *Client) update(originals, targets ResourceList, createApplyFunc CreateApplyFunc, updateApplyFunc UpdateApplyFunc) (*Result, error) { + updateErrors := []error{} res := &Result{} - c.Log("checking %d resources for changes", len(target)) - err := target.Visit(func(info *resource.Info, err error) error { + c.Logger().Debug("checking resources for changes", "resources", len(targets)) + err := targets.Visit(func(target *resource.Info, err error) error { if err != nil { return err } - helper := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()) - if _, err := helper.Get(info.Namespace, info.Name); err != nil { + helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) + if _, err := helper.Get(target.Namespace, target.Name); err != nil { if !apierrors.IsNotFound(err) { - return errors.Wrap(err, "could not get information about the resource") + return fmt.Errorf("could not get information about the resource: %w", err) } - if c.Extender != nil { - if err := c.Extender.BeforeCreateResource(info); err != nil { - return err - } - } + // Append the created resource to the results, even if something fails + res.Created = append(res.Created, target) + // Since the resource does not exist, create it. - if _, err := createResource(info); err != nil { - return errors.Wrap(err, "failed to create resource") + if err := createApplyFunc(target); err != nil { + return fmt.Errorf("failed to create resource: %w", err) } - res.Created = append(res.Created, info) - - kind := info.Mapping.GroupVersionKind.Kind - c.Log("Created a new %s called %q in %s\n", kind, info.Name, info.Namespace) + kind := target.Mapping.GroupVersionKind.Kind + c.Logger().Debug( + "created a new resource", + slog.String("namespace", target.Namespace), + slog.String("name", target.Name), + slog.String("kind", kind), + ) return nil } - originalInfo := original.Get(info) - if originalInfo == nil { - kind := info.Mapping.GroupVersionKind.Kind - return errors.Errorf("no %s with the name %q found", kind, info.Name) + original := originals.Get(target) + if original == nil { + kind := target.Mapping.GroupVersionKind.Kind + return fmt.Errorf("original object %s with the name %q not found", kind, target.Name) } - if c.Extender != nil { - if err := c.Extender.BeforeUpdateResource(info); err != nil { - return err - } + if err := updateApplyFunc(original, target); err != nil { + updateErrors = append(updateErrors, err) } - if err := updateResource(c, info, originalInfo.Object, force); err != nil { - c.Log("error updating the resource %q:\n\t %v", info.Name, err) - updateErrors = append(updateErrors, err.Error()) - } else { - res.Updated = append(res.Updated, info) - } + // Because we check for errors later, append the info regardless + res.Updated = append(res.Updated, target) return nil }) @@ -461,98 +617,282 @@ func (c *Client) Update(original, target ResourceList, force bool, opts UpdateOp case err != nil: return res, err case len(updateErrors) != 0: - return res, errors.Errorf(strings.Join(updateErrors, " && ")) + return res, joinErrors(updateErrors, " && ") } - for _, info := range original.Difference(target) { - c.Log("Deleting %s %q in namespace %s...", info.Mapping.GroupVersionKind.Kind, info.Name, info.Namespace) + for _, info := range originals.Difference(targets) { + c.Logger().Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) if err := info.Get(); err != nil { - c.Log("Unable to get obj %q, err: %s", info.Name, err) + c.Logger().Debug( + "unable to get object", + slog.String("namespace", info.Namespace), + slog.String("name", info.Name), + slog.String("kind", info.Mapping.GroupVersionKind.Kind), + slog.Any("error", err), + ) continue } - - if err := releaseutil.CheckOwnership(info.Object, opts.ReleaseName, opts.ReleaseNamespace); err != nil { - c.Log("Skipping delete of %q due to unmatched ownership annotations: %s", info.Name, err) - continue - } - annotations, err := metadataAccessor.Annotations(info.Object) if err != nil { - c.Log("Unable to get annotations on %q, err: %s", info.Name, err) + c.Logger().Debug( + "unable to get annotations", + slog.String("namespace", info.Namespace), + slog.String("name", info.Name), + slog.String("kind", info.Mapping.GroupVersionKind.Kind), + slog.Any("error", err), + ) } if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy { - c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy) + c.Logger().Debug("skipping delete due to annotation", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, "annotation", ResourcePolicyAnno, "value", KeepPolicy) continue } - - if c.Extender != nil { - if err := c.Extender.BeforeDeleteResource(info); err != nil { - return res, err - } - } - if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil { - c.Log("Failed to delete %q, err: %s", info.ObjectName(), err) + c.Logger().Debug( + "failed to delete resource", + slog.String("namespace", info.Namespace), + slog.String("name", info.Name), + slog.String("kind", info.Mapping.GroupVersionKind.Kind), + slog.Any("error", err), + ) + if !apierrors.IsNotFound(err) { + updateErrors = append(updateErrors, fmt.Errorf("failed to delete resource %s: %w", info.Name, err)) + } continue } res.Deleted = append(res.Deleted, info) } + + if len(updateErrors) != 0 { + return res, joinErrors(updateErrors, " && ") + } return res, nil } -// Delete deletes Kubernetes resources specified in the resources list with -// background cascade deletion. It will attempt to delete all resources even -// if one or more fail and collect any errors. All successfully deleted items -// will be returned in the `Deleted` ResourceList that is part of the result. -func (c *Client) Delete(resources ResourceList, opts DeleteOptions) (*Result, []error) { - return rdelete(c, resources, metav1.DeletePropagationBackground, opts) +type clientUpdateOptions struct { + threeWayMergeForUnstructured bool + serverSideApply bool + forceReplace bool + forceConflicts bool + dryRun bool + fieldValidationDirective FieldValidationDirective + upgradeClientSideFieldManager bool } -// Delete deletes Kubernetes resources specified in the resources list with -// given deletion propagation policy. It will attempt to delete all resources even -// if one or more fail and collect any errors. All successfully deleted items -// will be returned in the `Deleted` ResourceList that is part of the result. -func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation, opts DeleteOptions) (*Result, []error) { - return rdelete(c, resources, policy, opts) +type ClientUpdateOption func(*clientUpdateOptions) error + +// ClientUpdateOptionThreeWayMergeForUnstructured enables performing three-way merge for unstructured objects +// Must not be enabled when ClientUpdateOptionServerSideApply is enabled +func ClientUpdateOptionThreeWayMergeForUnstructured(threeWayMergeForUnstructured bool) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + o.threeWayMergeForUnstructured = threeWayMergeForUnstructured + + return nil + } } -func rdelete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation, opts DeleteOptions) (*Result, []error) { - var errs []error - res := &Result{} - mtx := sync.Mutex{} - err := perform(resources, func(info *resource.Info) error { - if opts.SkipIfInvalidOwnership { - if err := info.Get(); err != nil { - c.Log("Skipping delete of %q due to inability to get the object from cluster: %s", info.Name, err) +// ClientUpdateOptionServerSideApply enables performing object apply server-side (default) +// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/ +// Must not be enabled when ClientUpdateOptionThreeWayMerge is enabled +// +// `forceConflicts` forces conflicts to be resolved (may be enabled when serverSideApply enabled only) +// see: https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts +func ClientUpdateOptionServerSideApply(serverSideApply, forceConflicts bool) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + if !serverSideApply && forceConflicts { + return fmt.Errorf("forceConflicts enabled when serverSideApply disabled") + } + + o.serverSideApply = serverSideApply + o.forceConflicts = forceConflicts + + return nil + } +} + +// ClientUpdateOptionForceReplace forces objects to be replaced rather than updated via patch +// Must not be enabled when ClientUpdateOptionForceConflicts is enabled +func ClientUpdateOptionForceReplace(forceReplace bool) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + o.forceReplace = forceReplace + + return nil + } +} + +// ClientUpdateOptionDryRun requests the server to perform non-mutating operations only +func ClientUpdateOptionDryRun(dryRun bool) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + o.dryRun = dryRun + + return nil + } +} + +// ClientUpdateOptionFieldValidationDirective specifies how API operations validate object's schema +// - For client-side apply: this is ignored +// - For server-side apply: the directive is sent to the server to perform the validation +// +// Defaults to `FieldValidationDirectiveStrict` +func ClientUpdateOptionFieldValidationDirective(fieldValidationDirective FieldValidationDirective) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + o.fieldValidationDirective = fieldValidationDirective + + return nil + } +} + +// ClientUpdateOptionUpgradeClientSideFieldManager specifies that resources client-side field manager should be upgraded to server-side apply +// (before applying the object server-side) +// This is required when upgrading a chart from client-side to server-side apply, otherwise the client-side field management remains. Conflicting with server-side applied updates. +// +// Note: +// if this option is specified, but the object is not managed by client-side field manager, it will be a no-op. However, the cost of fetching the objects will be incurred. +// +// see: +// - https://github.com/kubernetes/kubernetes/pull/112905 +// - `UpgradeManagedFields` / https://github.com/kubernetes/kubernetes/blob/f47e9696d7237f1011d23c9b55f6947e60526179/staging/src/k8s.io/client-go/util/csaupgrade/upgrade.go#L81 +func ClientUpdateOptionUpgradeClientSideFieldManager(upgradeClientSideFieldManager bool) ClientUpdateOption { + return func(o *clientUpdateOptions) error { + o.upgradeClientSideFieldManager = upgradeClientSideFieldManager + + return nil + } +} + +// Update takes the current list of objects and target list of objects and +// creates resources that don't already exist, updates resources that have been +// modified in the target configuration, and deletes resources from the current +// configuration that are not present in the target configuration. If an error +// occurs, a Result will still be returned with the error, containing all +// resource updates, creations, and deletions that were attempted. These can be +// used for cleanup or other logging purposes. +// +// The default is to use server-side apply, equivalent to: `ClientUpdateOptionServerSideApply(true)` +func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdateOption) (*Result, error) { + updateOptions := clientUpdateOptions{ + serverSideApply: true, // Default to server-side apply + fieldValidationDirective: FieldValidationDirectiveStrict, + } + + errs := make([]error, 0, len(options)) + for _, o := range options { + errs = append(errs, o(&updateOptions)) + } + if err := errors.Join(errs...); err != nil { + return &Result{}, fmt.Errorf("invalid client update option(s): %w", err) + } + + if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply { + return &Result{}, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together") + } + + if updateOptions.forceConflicts && updateOptions.forceReplace { + return &Result{}, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together") + } + + if updateOptions.serverSideApply && updateOptions.forceReplace { + return &Result{}, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together") + } + + createApplyFunc := c.makeCreateApplyFunc( + updateOptions.serverSideApply, + updateOptions.forceConflicts, + updateOptions.dryRun, + updateOptions.fieldValidationDirective) + + makeUpdateApplyFunc := func() UpdateApplyFunc { + if updateOptions.forceReplace { + c.Logger().Debug( + "using resource replace update strategy", + slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective))) + return func(original, target *resource.Info) error { + if err := replaceResource(target, updateOptions.fieldValidationDirective); err != nil { + c.Logger().With( + slog.String("namespace", target.Namespace), + slog.String("name", target.Name), + slog.String("gvk", target.Mapping.GroupVersionKind.String()), + ).Debug( + "error replacing the resource", slog.Any("error", err), + ) + return err + } + + originalObject := original.Object + kind := target.Mapping.GroupVersionKind.Kind + c.Logger().Debug("replace succeeded", "name", original.Name, "initialKind", originalObject.GetObjectKind().GroupVersionKind().Kind, "kind", kind) + return nil } + } else if updateOptions.serverSideApply { + c.Logger().Debug( + "using server-side apply for resource update", + slog.Bool("forceConflicts", updateOptions.forceConflicts), + slog.Bool("dryRun", updateOptions.dryRun), + slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective)), + slog.Bool("upgradeClientSideFieldManager", updateOptions.upgradeClientSideFieldManager)) + return func(original, target *resource.Info) error { + + logger := c.Logger().With( + slog.String("namespace", target.Namespace), + slog.String("name", target.Name), + slog.String("gvk", target.Mapping.GroupVersionKind.String())) + + if updateOptions.upgradeClientSideFieldManager { + patched, err := upgradeClientSideFieldManager(original, updateOptions.dryRun, updateOptions.fieldValidationDirective) + if err != nil { + c.Logger().Debug("Error patching resource to replace CSA field management", slog.Any("error", err)) + return err + } + + if patched { + logger.Debug("Upgraded object client-side field management with server-side apply field management") + } + } + + if err := patchResourceServerSide(target, updateOptions.dryRun, updateOptions.forceConflicts, updateOptions.fieldValidationDirective); err != nil { + logger.Debug("Error patching resource", slog.Any("error", err)) + return err + } + + logger.Debug("Patched resource") - if err := releaseutil.CheckOwnership(info.Object, opts.ReleaseName, opts.ReleaseNamespace); err != nil { - c.Log("Skipping delete of %q due to unmatched ownership annotations: %s", info.Name, err) return nil } } - if c.Extender != nil { - if err := c.Extender.BeforeDeleteResource(info); err != nil { - mtx.Lock() - defer mtx.Unlock() - // Collect the error and continue on - errs = append(errs, err) - return nil - } + c.Logger().Debug("using client-side apply for resource update", slog.Bool("threeWayMergeForUnstructured", updateOptions.threeWayMergeForUnstructured)) + return func(original, target *resource.Info) error { + return patchResourceClientSide(original.Object, target, updateOptions.threeWayMergeForUnstructured) } + } + + return c.update(originals, targets, createApplyFunc, makeUpdateApplyFunc()) +} - c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) - err := deleteResource(info, propagation) +// Delete deletes Kubernetes resources specified in the resources list with +// given deletion propagation policy. It will attempt to delete all resources even +// if one or more fail and collect any errors. All successfully deleted items +// will be returned in the `Deleted` ResourceList that is part of the result. +func (c *Client) Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) { + var errs []error + res := &Result{} + mtx := sync.Mutex{} + err := perform(resources, func(target *resource.Info) error { + c.Logger().Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind) + err := deleteResource(target, policy) if err == nil || apierrors.IsNotFound(err) { if err != nil { - c.Log("Ignoring delete failure for %q %s: %v", info.Name, info.Mapping.GroupVersionKind, err) + c.Logger().Debug( + "ignoring delete failure", + slog.String("namespace", target.Namespace), + slog.String("name", target.Name), + slog.String("kind", target.Mapping.GroupVersionKind.Kind), + slog.Any("error", err)) } mtx.Lock() defer mtx.Unlock() - res.Deleted = append(res.Deleted, info) + res.Deleted = append(res.Deleted, target) return nil } mtx.Lock() @@ -568,51 +908,40 @@ func rdelete(c *Client, resources ResourceList, propagation metav1.DeletionPropa errs = append(errs, err) } if errs != nil { - return res, errs + return nil, errs } - - if opts.Wait { - var specs []*ResourcesWaiterDeleteResourceSpec - for _, resource := range res.Deleted { - specs = append(specs, &ResourcesWaiterDeleteResourceSpec{ - ResourceName: resource.Name, - Namespace: resource.Namespace, - GroupVersionResource: resource.Mapping.Resource, - }) - } - - if err := c.ResourcesWaiter.WaitUntilDeleted(context.Background(), specs, opts.WaitTimeout); err != nil { - return res, []error{fmt.Errorf("waiting until resources are deleted failed: %s", err)} - } - } - return res, nil } -func (c *Client) watchTimeout(t time.Duration) func(*resource.Info) error { - return func(info *resource.Info) error { - return c.watchUntilReady(t, info) +// https://github.com/kubernetes/kubectl/blob/197123726db24c61aa0f78d1f0ba6e91a2ec2f35/pkg/cmd/apply/apply.go#L439 +func isIncompatibleServerError(err error) bool { + // 415: Unsupported media type means we're talking to a server which doesn't + // support server-side apply. + if _, ok := err.(*apierrors.StatusError); !ok { + // Non-StatusError means the error isn't because the server is incompatible. + return false } + return err.(*apierrors.StatusError).Status().Code == http.StatusUnsupportedMediaType } -// WatchUntilReady watches the resources given and waits until it is ready. -// -// This method is mainly for hook implementations. It watches for a resource to -// hit a particular milestone. The milestone depends on the Kind. -// -// For most kinds, it checks to see if the resource is marked as Added or Modified -// by the Kubernetes event stream. For some kinds, it does more: -// -// - Jobs: A job is marked "Ready" when it has successfully completed. This is -// ascertained by watching the Status fields in a job's output. -// - Pods: A pod is marked "Ready" when it has successfully completed. This is -// ascertained by watching the status.phase field in a pod's output. -// -// Handling for other kinds will be added as necessary. -func (c *Client) WatchUntilReady(resources ResourceList, timeout time.Duration) error { - // For jobs, there's also the option to do poll c.Jobs(namespace).Get(): - // https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300 - return perform(resources, c.watchTimeout(timeout)) +// getManagedFieldsManager returns the manager string. If one was set it will be returned. +// Otherwise, one is calculated based on the name of the binary. +func getManagedFieldsManager() string { + + // When a manager is explicitly set use it + if ManagedFieldsManager != "" { + return ManagedFieldsManager + } + + // When no manager is set and no calling application can be found it is unknown + if len(os.Args[0]) == 0 { + return "unknown" + } + + // When there is an application that can be determined and no set manager + // use the base name. This is one of the ways Kubernetes libs handle figuring + // names out. + return filepath.Base(os.Args[0]) } func perform(infos ResourceList, fn func(*resource.Info) error) error { @@ -628,105 +957,89 @@ func perform(infos ResourceList, fn func(*resource.Info) error) error { for range infos { err := <-errs if err != nil { - result = multierror.Append(result, err) + result = errors.Join(result, err) } } return result } -// getManagedFieldsManager returns the manager string. If one was set it will be returned. -// Otherwise, one is calculated based on the name of the binary. -func getManagedFieldsManager() string { - - // When a manager is explicitly set use it - if ManagedFieldsManager != "" { - return ManagedFieldsManager - } - - // When no manager is set and no calling application can be found it is unknown - if len(os.Args[0]) == 0 { - return "unknown" - } - - // When there is an application that can be determined and no set manager - // use the base name. This is one of the ways Kubernetes libs handle figuring - // names out. - return filepath.Base(os.Args[0]) -} - func batchPerform(infos ResourceList, fn func(*resource.Info) error, errs chan<- error) { var kind string var wg sync.WaitGroup + defer wg.Wait() + for _, info := range infos { currentKind := info.Object.GetObjectKind().GroupVersionKind().Kind if kind != currentKind { wg.Wait() kind = currentKind } + wg.Add(1) - go func(i *resource.Info) { - errs <- fn(i) + go func(info *resource.Info) { + errs <- fn(info) wg.Done() }(info) } } -func createResource(info *resource.Info) (performResourceStatus, error) { - obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object) - if err != nil { - return resourceStatusUnknown, err - } - - return resourceStatusCreated, info.Refresh(obj, true) -} +var createMutex sync.Mutex -func createResourceSkipIfExists(info *resource.Info) (performResourceStatus, error) { - _, err := resource.NewHelper(info.Client, info.Mapping).Get(info.Namespace, info.Name) - if apierrors.IsNotFound(err) { - return createResource(info) - } else if err != nil { - return resourceStatusUnknown, err - } +func createResource(info *resource.Info) error { + return retry.RetryOnConflict( + retry.DefaultRetry, + func() error { + createMutex.Lock() + defer createMutex.Unlock() + obj, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).Create(info.Namespace, true, info.Object) + if err != nil { + return err + } - return resourceStatusUnknown, nil + return info.Refresh(obj, true) + }) } func deleteResource(info *resource.Info, policy metav1.DeletionPropagation) error { - opts := &metav1.DeleteOptions{PropagationPolicy: &policy} - _, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts) - return err + return retry.RetryOnConflict( + retry.DefaultRetry, + func() error { + opts := &metav1.DeleteOptions{PropagationPolicy: &policy} + _, err := resource.NewHelper(info.Client, info.Mapping).WithFieldManager(getManagedFieldsManager()).DeleteWithOptions(info.Namespace, info.Name, opts) + return err + }) } -func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.PatchType, error) { - oldData, err := json.Marshal(current) +func createPatch(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) ([]byte, types.PatchType, error) { + oldData, err := json.Marshal(original) if err != nil { - return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing current configuration") + return nil, types.StrategicMergePatchType, fmt.Errorf("serializing current configuration: %w", err) } newData, err := json.Marshal(target.Object) if err != nil { - return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing target configuration") + return nil, types.StrategicMergePatchType, fmt.Errorf("serializing target configuration: %w", err) } // Fetch the current object for the three way merge helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) currentObj, err := helper.Get(target.Namespace, target.Name) if err != nil && !apierrors.IsNotFound(err) { - return nil, types.StrategicMergePatchType, errors.Wrapf(err, "unable to get data for current object %s/%s", target.Namespace, target.Name) + return nil, types.StrategicMergePatchType, fmt.Errorf("unable to get data for current object %s/%s: %w", target.Namespace, target.Name, err) } // Even if currentObj is nil (because it was not found), it will marshal just fine currentData, err := json.Marshal(currentObj) if err != nil { - return nil, types.StrategicMergePatchType, errors.Wrap(err, "serializing live configuration") + return nil, types.StrategicMergePatchType, fmt.Errorf("serializing live configuration: %w", err) } // Get a versioned object versionedObject := AsVersioned(target) - // Unstructured objects, such as CRDs, may not have an not registered error + // Unstructured objects, such as CRDs, may not have a not registered error // returned from ConvertToVersion. Anything that's unstructured should - // use the jsonpatch.CreateMergePatch. Strategic Merge Patch is not supported + // use generic JSON merge patch. Strategic Merge Patch is not supported // on objects like CRDs. _, isUnstructured := versionedObject.(runtime.Unstructured) @@ -734,6 +1047,19 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P _, isCRD := versionedObject.(*apiextv1beta1.CustomResourceDefinition) if isUnstructured || isCRD { + if threeWayMergeForUnstructured { + // from https://github.com/kubernetes/kubectl/blob/b83b2ec7d15f286720bccf7872b5c72372cb8e80/pkg/cmd/apply/patcher.go#L129 + preconditions := []mergepatch.PreconditionFunc{ + mergepatch.RequireKeyUnchanged("apiVersion"), + mergepatch.RequireKeyUnchanged("kind"), + mergepatch.RequireMetadataKeyUnchanged("name"), + } + patch, err := jsonmergepatch.CreateThreeWayJSONMergePatch(oldData, newData, currentData, preconditions...) + if err != nil && mergepatch.IsPreconditionFailed(err) { + err = fmt.Errorf("%w: at least one field was changed: apiVersion, kind or name", err) + } + return patch, types.MergePatchType, err + } // fall back to generic JSON merge patch patch, err := jsonpatch.CreateMergePatch(oldData, newData) return patch, types.MergePatchType, err @@ -741,156 +1067,196 @@ func createPatch(target *resource.Info, current runtime.Object) ([]byte, types.P patchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObject) if err != nil { - return nil, types.StrategicMergePatchType, errors.Wrap(err, "unable to create patch metadata from object") + return nil, types.StrategicMergePatchType, fmt.Errorf("unable to create patch metadata from object: %w", err) } patch, err := strategicpatch.CreateThreeWayMergePatch(oldData, newData, currentData, patchMeta, true) return patch, types.StrategicMergePatchType, err } -func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error { - var ( - obj runtime.Object - helper = resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) - kind = target.Mapping.GroupVersionKind.Kind - ) +func replaceResource(target *resource.Info, fieldValidationDirective FieldValidationDirective) error { - // if --force is applied, attempt to replace the existing resource with the new object. - if force { - var err error - obj, err = helper.Replace(target.Namespace, target.Name, true, target.Object) - if err != nil { - return errors.Wrap(err, "failed to replace object") - } - c.Log("Replaced %q with kind %s for kind %s", target.Name, currentObj.GetObjectKind().GroupVersionKind().Kind, kind) - } else { - patch, patchType, err := createPatch(target, currentObj) - if err != nil { - return errors.Wrap(err, "failed to create patch") - } + helper := resource.NewHelper(target.Client, target.Mapping). + WithFieldValidation(string(fieldValidationDirective)). + WithFieldManager(getManagedFieldsManager()) - if patch == nil || string(patch) == "{}" { - c.Log("Looks like there are no changes for %s %q", kind, target.Name) - // This needs to happen to make sure that Helm has the latest info from the API - // Otherwise there will be no labels and other functions that use labels will panic - if err := target.Get(); err != nil { - return errors.Wrap(err, "failed to refresh resource information") - } - return nil - } - // send patch to server - c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace) - obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil) - if err != nil { - return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind) - } + obj, err := helper.Replace(target.Namespace, target.Name, true, target.Object) + if err != nil { + return fmt.Errorf("failed to replace object: %w", err) + } + + if err := target.Refresh(obj, true); err != nil { + return fmt.Errorf("failed to refresh object after replace: %w", err) } - target.Refresh(obj, true) return nil + } -func (c *Client) watchUntilReady(timeout time.Duration, info *resource.Info) error { - kind := info.Mapping.GroupVersionKind.Kind - switch kind { - case "Job", "Pod": - default: - return nil +func patchResourceClientSide(original runtime.Object, target *resource.Info, threeWayMergeForUnstructured bool) error { + + patch, patchType, err := createPatch(original, target, threeWayMergeForUnstructured) + if err != nil { + return fmt.Errorf("failed to create patch: %w", err) } - c.Log("Watching for changes to %s %s with timeout of %v", kind, info.Name, timeout) + kind := target.Mapping.GroupVersionKind.Kind + if patch == nil || string(patch) == "{}" { + slog.Debug("no changes detected", "kind", kind, "name", target.Name) + // This needs to happen to make sure that Helm has the latest info from the API + // Otherwise there will be no labels and other functions that use labels will panic + if err := target.Get(); err != nil { + return fmt.Errorf("failed to refresh resource information: %w", err) + } + return nil + } - // Use a selector on the name of the resource. This should be unique for the - // given version and kind - selector, err := fields.ParseSelector(fmt.Sprintf("metadata.name=%s", info.Name)) + // send patch to server + slog.Debug("patching resource", "kind", kind, "name", target.Name, "namespace", target.Namespace) + helper := resource.NewHelper(target.Client, target.Mapping).WithFieldManager(getManagedFieldsManager()) + obj, err := helper.Patch(target.Namespace, target.Name, patchType, patch, nil) if err != nil { - return err + return fmt.Errorf("cannot patch %q with kind %s: %w", target.Name, kind, err) } - lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, selector) - - // What we watch for depends on the Kind. - // - For a Job, we watch for completion. - // - For all else, we watch until Ready. - // In the future, we might want to add some special logic for types - // like Ingress, Volume, etc. - - ctx, cancel := watchtools.ContextWithOptionalTimeout(context.Background(), timeout) - defer cancel() - _, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) { - // Make sure the incoming object is versioned as we use unstructured - // objects when we build manifests - obj := convertWithMapper(e.Object, info.Mapping) - switch e.Type { - case watch.Added, watch.Modified: - // For things like a secret or a config map, this is the best indicator - // we get. We care mostly about jobs, where what we want to see is - // the status go into a good state. For other types, like ReplicaSet - // we don't really do anything to support these as hooks. - c.Log("Add/Modify event for %s: %v", info.Name, e.Type) - switch kind { - case "Job": - return c.waitForJob(obj, info.Name) - case "Pod": - return c.waitForPodSuccess(obj, info.Name) + + target.Refresh(obj, true) + + return nil +} + +// upgradeClientSideFieldManager is simply a wrapper around csaupgrade.UpgradeManagedFields +// that upgrade CSA managed fields to SSA apply +// see: https://github.com/kubernetes/kubernetes/pull/112905 +func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValidationDirective FieldValidationDirective) (bool, error) { + + fieldManagerName := getManagedFieldsManager() + + patched := false + err := retry.RetryOnConflict( + retry.DefaultRetry, + func() error { + + if err := info.Get(); err != nil { + return fmt.Errorf("failed to get object %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) } - return true, nil - case watch.Deleted: - c.Log("Deleted event for %s", info.Name) - return true, nil - case watch.Error: - // Handle error and return with an error. - c.Log("Error event for %s", info.Name) - return true, errors.Errorf("failed to deploy %s", info.Name) - default: - return false, nil - } - }) - return err + + helper := resource.NewHelper( + info.Client, + info.Mapping). + DryRun(dryRun). + WithFieldManager(fieldManagerName). + WithFieldValidation(string(fieldValidationDirective)) + + patchData, err := csaupgrade.UpgradeManagedFieldsPatch( + info.Object, + sets.New(fieldManagerName), + fieldManagerName) + if err != nil { + return fmt.Errorf("failed to upgrade managed fields for object %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) + } + + if len(patchData) == 0 { + return nil + } + + obj, err := helper.Patch( + info.Namespace, + info.Name, + types.JSONPatchType, + patchData, + nil) + + if err == nil { + patched = true + return info.Refresh(obj, true) + } + + if !apierrors.IsConflict(err) { + return fmt.Errorf("failed to patch object to upgrade CSA field manager %s/%s %s: %w", info.Namespace, info.Name, info.Mapping.GroupVersionKind.String(), err) + } + + return err + }) + + return patched, err } -// waitForJob is a helper that waits for a job to complete. -// -// This operates on an event returned from a watcher. -func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) { - o, ok := obj.(*batch.Job) - if !ok { - return true, errors.Errorf("expected %s to be a *batch.Job, got %T", name, obj) +// Patch reource using server-side apply +func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error { + helper := resource.NewHelper( + target.Client, + target.Mapping). + DryRun(dryRun). + WithFieldManager(getManagedFieldsManager()). + WithFieldValidation(string(fieldValidationDirective)) + + // Send the full object to be applied on the server side. + data, err := runtime.Encode(unstructured.UnstructuredJSONScheme, target.Object) + if err != nil { + return fmt.Errorf("failed to encode object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err) + } + options := metav1.PatchOptions{ + Force: &forceConflicts, } + obj, err := helper.Patch( + target.Namespace, + target.Name, + types.ApplyPatchType, + data, + &options, + ) + if err != nil { + if isIncompatibleServerError(err) { + return fmt.Errorf("server-side apply not available on the server: %v", err) + } - for _, c := range o.Status.Conditions { - if c.Type == batch.JobComplete && c.Status == "True" { - return true, nil - } else if c.Type == batch.JobFailed && c.Status == "True" { - return true, errors.Errorf("job %s failed: %s", name, c.Reason) + if apierrors.IsConflict(err) { + return fmt.Errorf("conflict occurred while applying object %s/%s %s: %w", target.Namespace, target.Name, target.Mapping.GroupVersionKind.String(), err) } + + return err } - c.Log("%s: Jobs active: %d, jobs failed: %d, jobs succeeded: %d", name, o.Status.Active, o.Status.Failed, o.Status.Succeeded) - return false, nil + return target.Refresh(obj, true) } -// waitForPodSuccess is a helper that waits for a pod to complete. -// -// This operates on an event returned from a watcher. -func (c *Client) waitForPodSuccess(obj runtime.Object, name string) (bool, error) { - o, ok := obj.(*v1.Pod) - if !ok { - return true, errors.Errorf("expected %s to be a *v1.Pod, got %T", name, obj) +// GetPodList uses the kubernetes interface to get the list of pods filtered by listOptions +func (c *Client) GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) { + podList, err := c.kubeClient.CoreV1().Pods(namespace).List(context.Background(), listOptions) + if err != nil { + return nil, fmt.Errorf("failed to get pod list with options: %+v with error: %v", listOptions, err) } + return podList, nil +} - switch o.Status.Phase { - case v1.PodSucceeded: - c.Log("Pod %s succeeded", o.Name) - return true, nil - case v1.PodFailed: - return true, errors.Errorf("pod %s failed", o.Name) - case v1.PodPending: - c.Log("Pod %s pending", o.Name) - case v1.PodRunning: - c.Log("Pod %s running", o.Name) +// OutputContainerLogsForPodList is a helper that outputs logs for a list of pods +func (c *Client) OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error { + for _, pod := range podList.Items { + for _, container := range pod.Spec.Containers { + options := &v1.PodLogOptions{ + Container: container.Name, + } + request := c.kubeClient.CoreV1().Pods(namespace).GetLogs(pod.Name, options) + err2 := copyRequestStreamToWriter(request, pod.Name, container.Name, writerFunc(namespace, pod.Name, container.Name)) + if err2 != nil { + return err2 + } + } } + return nil +} - return false, nil +func copyRequestStreamToWriter(request *rest.Request, podName, containerName string, writer io.Writer) error { + readCloser, err := request.Stream(context.Background()) + if err != nil { + return fmt.Errorf("failed to stream pod logs for pod: %s, container: %s", podName, containerName) + } + defer readCloser.Close() + _, err = io.Copy(writer, readCloser) + if err != nil { + return fmt.Errorf("failed to copy IO from logs for pod: %s, container: %s", podName, containerName) + } + return nil } // scrubValidationError removes kubectl info from the message. @@ -906,120 +1272,26 @@ func scrubValidationError(err error) error { return err } -// WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase -// and returns said phase (PodSucceeded or PodFailed qualify). -func (c *Client) WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) { - client, err := c.getKubeClient() - if err != nil { - return v1.PodUnknown, err - } - to := int64(timeout) - watcher, err := client.CoreV1().Pods(c.namespace()).Watch(context.Background(), metav1.ListOptions{ - FieldSelector: fmt.Sprintf("metadata.name=%s", name), - TimeoutSeconds: &to, - }) - if err != nil { - return v1.PodUnknown, err - } - - for event := range watcher.ResultChan() { - p, ok := event.Object.(*v1.Pod) - if !ok { - return v1.PodUnknown, fmt.Errorf("%s not a pod", name) - } - switch p.Status.Phase { - case v1.PodFailed: - return v1.PodFailed, nil - case v1.PodSucceeded: - return v1.PodSucceeded, nil - } - } - - return v1.PodUnknown, err +type joinedErrors struct { + errs []error + sep string } -type performResourceStatus int - -const ( - resourceStatusUnknown performResourceStatus = iota - resourceStatusCreated - resourceStatusUpdated - resourceStatusDeleted -) - -func performWithResult(infos ResourceList, fn func(*resource.Info) (performResourceStatus, error)) (*Result, error) { - if len(infos) == 0 { - return &Result{}, ErrNoObjectsVisited - } - - infosByGK := groupInfosByGK(infos) - - type performResult struct { - resource *resource.Info - status performResourceStatus - error error - } - - result := &Result{} - - for _, resList := range infosByGK { - performResultsCh := make(chan performResult, len(resList)) - for _, res := range resList { - resC := res - go func() { - status, err := fn(resC) - performResultsCh <- performResult{ - resource: resC, - status: status, - error: err, - } - }() - } - - var errs []error - for range resList { - perfRes := <-performResultsCh - - if perfRes.error != nil { - errs = append(errs, perfRes.error) - continue - } - - switch perfRes.status { - case resourceStatusUnknown: - case resourceStatusCreated: - result.Created = append(result.Created, perfRes.resource) - case resourceStatusUpdated: - result.Updated = append(result.Updated, perfRes.resource) - case resourceStatusDeleted: - result.Deleted = append(result.Deleted, perfRes.resource) - default: - panic("unexpected status") - } - } - - if len(errs) > 0 { - return result, errs[0] - } +func joinErrors(errs []error, sep string) error { + return &joinedErrors{ + errs: errs, + sep: sep, } - - return result, nil } -func groupInfosByGK(infos ResourceList) []ResourceList { - var infosByGK []ResourceList - var lastGK schema.GroupKind - for _, info := range infos { - currentGK := info.Object.GetObjectKind().GroupVersionKind().GroupKind() - if lastGK == currentGK { - infosByGK[len(infosByGK)-1].Append(info) - } else { - rl := ResourceList{} - rl.Append(info) - infosByGK = append(infosByGK, rl) - lastGK = currentGK - } +func (e *joinedErrors) Error() string { + errs := make([]string, 0, len(e.errs)) + for _, err := range e.errs { + errs = append(errs, err.Error()) } + return strings.Join(errs, e.sep) +} - return infosByGK +func (e *joinedErrors) Unwrap() []error { + return e.errs } diff --git a/pkg/helm/pkg/kube/client_extender.go b/pkg/helm/pkg/kube/client_extender.go deleted file mode 100644 index 1ee6edab..00000000 --- a/pkg/helm/pkg/kube/client_extender.go +++ /dev/null @@ -1,9 +0,0 @@ -package kube - -import "k8s.io/cli-runtime/pkg/resource" - -type ClientExtender interface { - BeforeCreateResource(info *resource.Info) error - BeforeUpdateResource(info *resource.Info) error - BeforeDeleteResource(info *resource.Info) error -} diff --git a/pkg/helm/pkg/kube/client_test.go b/pkg/helm/pkg/kube/client_test.go index a1f224d1..c44b0d7d 100644 --- a/pkg/helm/pkg/kube/client_test.go +++ b/pkg/helm/pkg/kube/client_test.go @@ -18,22 +18,49 @@ package kube import ( "bytes" + "context" + "errors" + "fmt" "io" "net/http" "strings" + "sync" "testing" + "time" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + "github.com/fluxcd/cli-utils/pkg/object" + "github.com/fluxcd/cli-utils/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" + dynamicfake "k8s.io/client-go/dynamic/fake" + "k8s.io/client-go/kubernetes" + k8sfake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" ) -var unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer -var codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) +var ( + unstructuredSerializer = resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer + codec = scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) +) func objBody(obj runtime.Object) io.ReadCloser { return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(codec, obj)))) @@ -90,108 +117,223 @@ func newResponse(code int, obj runtime.Object) (*http.Response, error) { return &http.Response{StatusCode: code, Header: header, Body: body}, nil } +func newResponseJSON(code int, json []byte) (*http.Response, error) { + header := http.Header{} + header.Set("Content-Type", runtime.ContentTypeJSON) + body := io.NopCloser(bytes.NewReader(json)) + return &http.Response{StatusCode: code, Header: header, Body: body}, nil +} + func newTestClient(t *testing.T) *Client { + t.Helper() testFactory := cmdtesting.NewTestFactory() t.Cleanup(testFactory.Cleanup) return &Client{ - Factory: testFactory.WithNamespace("default"), - Log: nopLogger, + Factory: testFactory.WithNamespace(v1.NamespaceDefault), } } -func TestUpdate(t *testing.T) { - listA := newPodList("starfish", "otter", "squid") - listB := newPodList("starfish", "otter", "dolphin") - listC := newPodList("starfish", "otter", "dolphin") - listB.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} - listC.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} +type RequestResponseAction struct { + Request http.Request + Response http.Response + Error error +} - var actions []string +type RoundTripperTestFunc func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) - c := newTestClient(t) - c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{ - NegotiatedSerializer: unstructuredSerializer, - Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - p, m := req.URL.Path, req.Method - actions = append(actions, p+":"+m) - t.Logf("got request %s %s", p, m) - switch { - case p == "/namespaces/default/pods/starfish" && m == "GET": - return newResponse(200, &listA.Items[0]) - case p == "/namespaces/default/pods/otter" && m == "GET": - return newResponse(200, &listA.Items[1]) - case p == "/namespaces/default/pods/otter" && m == "PATCH": - data, err := io.ReadAll(req.Body) - if err != nil { - t.Fatalf("could not dump request: %s", err) - } - req.Body.Close() - expected := `{}` - if string(data) != expected { - t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data)) - } - return newResponse(200, &listB.Items[0]) - case p == "/namespaces/default/pods/dolphin" && m == "GET": - return newResponse(404, notFoundBody()) - case p == "/namespaces/default/pods/starfish" && m == "PATCH": - data, err := io.ReadAll(req.Body) - if err != nil { - t.Fatalf("could not dump request: %s", err) - } - req.Body.Close() - expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}` - if string(data) != expected { - t.Errorf("expected patch\n%s\ngot\n%s", expected, string(data)) - } - return newResponse(200, &listB.Items[0]) - case p == "/namespaces/default/pods" && m == "POST": - return newResponse(200, &listB.Items[1]) - case p == "/namespaces/default/pods/squid" && m == "DELETE": - return newResponse(200, &listB.Items[1]) - case p == "/namespaces/default/pods/squid" && m == "GET": - return newResponse(200, &listB.Items[2]) - default: - t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) - return nil, nil - } - }), +func NewRequestResponseLogClient(t *testing.T, cb RoundTripperTestFunc) RequestResponseLogClient { + t.Helper() + return RequestResponseLogClient{ + t: t, + cb: cb, } - first, err := c.Build(objBody(&listA), false) - if err != nil { - t.Fatal(err) +} + +// RequestResponseLogClient is a test client that logs requests and responses +// Satisfying http.RoundTripper interface, it can be used to mock HTTP requests in tests. +// Forwarding requests to a callback function (cb) that can be used to simulate server responses. +type RequestResponseLogClient struct { + t *testing.T + cb RoundTripperTestFunc + actionsLock sync.Mutex + Actions []RequestResponseAction +} + +func (r *RequestResponseLogClient) Do(req *http.Request) (*http.Response, error) { + t := r.t + t.Helper() + + readBodyBytes := func(body io.ReadCloser) []byte { + if body == nil { + return []byte{} + } + + defer body.Close() + bodyBytes, err := io.ReadAll(body) + require.NoError(t, err) + + return bodyBytes } - second, err := c.Build(objBody(&listB), false) - if err != nil { - t.Fatal(err) + + reqBytes := readBodyBytes(req.Body) + + t.Logf("Request: %s %s %s", req.Method, req.URL.String(), reqBytes) + if req.Body != nil { + req.Body = io.NopCloser(bytes.NewReader(reqBytes)) } - result, err := c.Update(first, second, false, UpdateOptions{}) - if err != nil { - t.Fatal(err) + resp, err := r.cb(r.Actions, req) + + respBytes := readBodyBytes(resp.Body) + t.Logf("Response: %d %s", resp.StatusCode, string(respBytes)) + if resp.Body != nil { + resp.Body = io.NopCloser(bytes.NewReader(respBytes)) } - if len(result.Created) != 1 { - t.Errorf("expected 1 resource created, got %d", len(result.Created)) + r.actionsLock.Lock() + defer r.actionsLock.Unlock() + r.Actions = append(r.Actions, RequestResponseAction{ + Request: *req, + Response: *resp, + Error: err, + }) + + return resp, err +} + +func TestCreate(t *testing.T) { + // Note: c.Create with the fake client can currently only test creation of a single pod/object in the same list. When testing + // with more than one pod, c.Create will run into a data race as it calls perform->batchPerform which performs creation + // in batches. The race is something in the fake client itself in `func (c *RESTClient) do(...)` + // when it stores the req: c.Req = req and cannot (?) be fixed easily. + + type testCase struct { + Name string + Pods v1.PodList + Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) + ServerSideApply bool + ExpectedActions []string + ExpectedErrorContains string + } + + testCases := map[string]testCase{ + "Create success (client-side apply)": { + Pods: newPodList("starfish"), + ServerSideApply: false, + Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, _ *http.Request) (*http.Response, error) { + t.Helper() + + if len(previous) < 2 { // simulate a conflict + return newResponseJSON(http.StatusConflict, resourceQuotaConflict) + } + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + ExpectedActions: []string{ + "/namespaces/default/pods:POST", + "/namespaces/default/pods:POST", + "/namespaces/default/pods:POST", + }, + }, + "Create success (server-side apply)": { + Pods: newPodList("whale"), + ServerSideApply: true, + Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, _ *http.Request) (*http.Response, error) { + t.Helper() + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + ExpectedActions: []string{ + "/namespaces/default/pods/whale:PATCH", + }, + }, + "Create fail: incompatible server (server-side apply)": { + Pods: newPodList("lobster"), + ServerSideApply: true, + Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + return &http.Response{ + StatusCode: http.StatusUnsupportedMediaType, + Request: req, + }, nil + }, + ExpectedErrorContains: "server-side apply not available on the server:", + ExpectedActions: []string{ + "/namespaces/default/pods/lobster:PATCH", + }, + }, + "Create fail: quota (server-side apply)": { + Pods: newPodList("dolphin"), + ServerSideApply: true, + Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, _ *http.Request) (*http.Response, error) { + t.Helper() + + return newResponseJSON(http.StatusConflict, resourceQuotaConflict) + }, + ExpectedErrorContains: "Operation cannot be fulfilled on resourcequotas \"quota\": the object has been modified; " + + "please apply your changes to the latest version and try again", + ExpectedActions: []string{ + "/namespaces/default/pods/dolphin:PATCH", + }, + }, } - if len(result.Updated) != 2 { - t.Errorf("expected 2 resource updated, got %d", len(result.Updated)) + + c := newTestClient(t) + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + + client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + return tc.Callback(t, tc, previous, req) + }) + + c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(client.Do), + } + + list, err := c.Build(objBody(&tc.Pods), false) + require.NoError(t, err) + if err != nil { + t.Fatal(err) + } + + result, err := c.Create( + list, + ClientCreateOptionServerSideApply(tc.ServerSideApply, false)) + if tc.ExpectedErrorContains != "" { + require.ErrorContains(t, err, tc.ExpectedErrorContains) + } else { + require.NoError(t, err) + + // See note above about limitations in supporting more than a single object + assert.Len(t, result.Created, 1, "expected 1 object created, got %d", len(result.Created)) + } + + actions := []string{} + for _, action := range client.Actions { + path, method := action.Request.URL.Path, action.Request.Method + actions = append(actions, path+":"+method) + } + + assert.Equal(t, tc.ExpectedActions, actions) + + }) } - if len(result.Deleted) != 1 { - t.Errorf("expected 1 resource deleted, got %d", len(result.Deleted)) +} + +func TestUpdate(t *testing.T) { + type testCase struct { + OriginalPods v1.PodList + TargetPods v1.PodList + ThreeWayMergeForUnstructured bool + ServerSideApply bool + ExpectedActions []string + ExpectedError string } - // TODO: Find a way to test methods that use Client Set - // Test with a wait - // if err := c.Update("test", objBody(codec, &listB), objBody(codec, &listC), false, 300, true); err != nil { - // t.Fatal(err) - // } - // Test with a wait should fail - // TODO: A way to make this not based off of an extremely short timeout? - // if err := c.Update("test", objBody(codec, &listC), objBody(codec, &listA), false, 2, true); err != nil { - // t.Fatal(err) - // } - expectedActions := []string{ + expectedActionsClientSideApply := []string{ "/namespaces/default/pods/starfish:GET", "/namespaces/default/pods/starfish:GET", "/namespaces/default/pods/starfish:PATCH", @@ -199,17 +341,204 @@ func TestUpdate(t *testing.T) { "/namespaces/default/pods/otter:GET", "/namespaces/default/pods/otter:GET", "/namespaces/default/pods/dolphin:GET", - "/namespaces/default/pods:POST", + "/namespaces/default/pods:POST", // create dolphin + "/namespaces/default/pods:POST", // retry due to 409 + "/namespaces/default/pods:POST", // retry due to 409 "/namespaces/default/pods/squid:GET", "/namespaces/default/pods/squid:DELETE", + "/namespaces/default/pods/notfound:GET", + "/namespaces/default/pods/notfound:DELETE", } - if len(expectedActions) != len(actions) { - t.Fatalf("unexpected number of requests, expected %d, got %d", len(expectedActions), len(actions)) + + expectedActionsServerSideApply := []string{ + "/namespaces/default/pods/starfish:GET", + "/namespaces/default/pods/starfish:GET", + "/namespaces/default/pods/starfish:PATCH", + "/namespaces/default/pods/otter:GET", + "/namespaces/default/pods/otter:GET", + "/namespaces/default/pods/otter:PATCH", + "/namespaces/default/pods/dolphin:GET", + "/namespaces/default/pods/dolphin:PATCH", // create dolphin + "/namespaces/default/pods/squid:GET", + "/namespaces/default/pods/squid:DELETE", + "/namespaces/default/pods/notfound:GET", + "/namespaces/default/pods/notfound:DELETE", } - for k, v := range expectedActions { - if actions[k] != v { - t.Errorf("expected %s request got %s", v, actions[k]) - } + + testCases := map[string]testCase{ + "client-side apply": { + OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), + TargetPods: func() v1.PodList { + listTarget := newPodList("starfish", "otter", "dolphin") + listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return listTarget + }(), + ThreeWayMergeForUnstructured: false, + ServerSideApply: false, + ExpectedActions: expectedActionsClientSideApply, + ExpectedError: "", + }, + "client-side apply (three-way merge for unstructured)": { + OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), + TargetPods: func() v1.PodList { + listTarget := newPodList("starfish", "otter", "dolphin") + listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return listTarget + }(), + ThreeWayMergeForUnstructured: true, + ServerSideApply: false, + ExpectedActions: expectedActionsClientSideApply, + ExpectedError: "", + }, + "serverSideApply": { + OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), + TargetPods: func() v1.PodList { + listTarget := newPodList("starfish", "otter", "dolphin") + listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return listTarget + }(), + ThreeWayMergeForUnstructured: false, + ServerSideApply: true, + ExpectedActions: expectedActionsServerSideApply, + ExpectedError: "", + }, + "serverSideApply with forbidden deletion": { + OriginalPods: newPodList("starfish", "otter", "squid", "notfound", "forbidden"), + TargetPods: func() v1.PodList { + listTarget := newPodList("starfish", "otter", "dolphin") + listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return listTarget + }(), + ThreeWayMergeForUnstructured: false, + ServerSideApply: true, + ExpectedActions: append(expectedActionsServerSideApply, + "/namespaces/default/pods/forbidden:GET", + "/namespaces/default/pods/forbidden:DELETE", + ), + ExpectedError: "failed to delete resource forbidden:", + }, + } + + c := newTestClient(t) + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + + listOriginal := tc.OriginalPods + listTarget := tc.TargetPods + + iterationCounter := 0 + cb := func(_ []RequestResponseAction, req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + + switch { + case p == "/namespaces/default/pods/starfish" && m == http.MethodGet: + return newResponse(http.StatusOK, &listOriginal.Items[0]) + case p == "/namespaces/default/pods/otter" && m == http.MethodGet: + return newResponse(http.StatusOK, &listOriginal.Items[1]) + case p == "/namespaces/default/pods/otter" && m == http.MethodPatch: + if !tc.ServerSideApply { + defer req.Body.Close() + data, err := io.ReadAll(req.Body) + require.NoError(t, err) + + assert.Equal(t, `{}`, string(data)) + } + + return newResponse(http.StatusOK, &listTarget.Items[0]) + case p == "/namespaces/default/pods/dolphin" && m == http.MethodGet: + return newResponse(http.StatusNotFound, notFoundBody()) + case p == "/namespaces/default/pods/starfish" && m == http.MethodPatch: + if !tc.ServerSideApply { + // Ensure client-side apply specifies correct patch + defer req.Body.Close() + data, err := io.ReadAll(req.Body) + require.NoError(t, err) + + expected := `{"spec":{"$setElementOrder/containers":[{"name":"app:v4"}],"containers":[{"$setElementOrder/ports":[{"containerPort":443}],"name":"app:v4","ports":[{"containerPort":443,"name":"https"},{"$patch":"delete","containerPort":80}]}]}}` + assert.Equal(t, expected, string(data)) + } + + return newResponse(http.StatusOK, &listTarget.Items[0]) + case p == "/namespaces/default/pods" && m == http.MethodPost: + if iterationCounter < 2 { + iterationCounter++ + return newResponseJSON(http.StatusConflict, resourceQuotaConflict) + } + + return newResponse(http.StatusOK, &listTarget.Items[1]) + case p == "/namespaces/default/pods/dolphin" && m == http.MethodPatch: + return newResponse(http.StatusOK, &listTarget.Items[1]) + case p == "/namespaces/default/pods/squid" && m == http.MethodDelete: + return newResponse(http.StatusOK, &listTarget.Items[1]) + case p == "/namespaces/default/pods/squid" && m == http.MethodGet: + return newResponse(http.StatusOK, &listTarget.Items[2]) + case p == "/namespaces/default/pods/notfound" && m == http.MethodGet: + // Resource exists in original but will simulate not found on delete + return newResponse(http.StatusOK, &listOriginal.Items[3]) + case p == "/namespaces/default/pods/notfound" && m == http.MethodDelete: + // Simulate a not found during deletion; should not cause update to fail + return newResponse(http.StatusNotFound, notFoundBody()) + case p == "/namespaces/default/pods/forbidden" && m == http.MethodGet: + return newResponse(http.StatusOK, &listOriginal.Items[4]) + case p == "/namespaces/default/pods/forbidden" && m == http.MethodDelete: + // Simulate RBAC forbidden that should cause update to fail + return newResponse(http.StatusForbidden, &metav1.Status{ + Status: metav1.StatusFailure, + Message: "pods \"forbidden\" is forbidden: User \"test-user\" cannot delete resource \"pods\" in API group \"\" in the namespace \"default\"", + Reason: metav1.StatusReasonForbidden, + Code: http.StatusForbidden, + }) + } + + t.FailNow() + return nil, nil + } + + client := NewRequestResponseLogClient(t, cb) + + c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(client.Do), + } + + first, err := c.Build(objBody(&listOriginal), false) + require.NoError(t, err) + + second, err := c.Build(objBody(&listTarget), false) + require.NoError(t, err) + + result, err := c.Update( + first, + second, + ClientUpdateOptionThreeWayMergeForUnstructured(tc.ThreeWayMergeForUnstructured), + ClientUpdateOptionForceReplace(false), + ClientUpdateOptionServerSideApply(tc.ServerSideApply, false), + ClientUpdateOptionUpgradeClientSideFieldManager(true)) + + if tc.ExpectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.ExpectedError) + } else { + require.NoError(t, err) + } + + assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) + assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated)) + assert.Len(t, result.Deleted, 1, "expected 1 resource deleted, got %d", len(result.Deleted)) + + actions := []string{} + for _, action := range client.Actions { + path, method := action.Request.URL.Path, action.Request.Method + actions = append(actions, path+":"+method) + } + + assert.Equal(t, tc.ExpectedActions, actions) + }) } } @@ -341,6 +670,219 @@ func TestPerform(t *testing.T) { } } +func TestWait(t *testing.T) { + podList := newPodList("starfish", "otter", "squid") + + var created *time.Time + + c := newTestClient(t) + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + switch { + case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet: + pod := &podList.Items[0] + if created != nil && time.Since(*created) >= time.Second*5 { + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + } + } + return newResponse(http.StatusOK, pod) + case p == "/api/v1/namespaces/default/pods/otter" && m == http.MethodGet: + pod := &podList.Items[1] + if created != nil && time.Since(*created) >= time.Second*5 { + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + } + } + return newResponse(http.StatusOK, pod) + case p == "/api/v1/namespaces/default/pods/squid" && m == http.MethodGet: + pod := &podList.Items[2] + if created != nil && time.Since(*created) >= time.Second*5 { + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + } + } + return newResponse(http.StatusOK, pod) + case p == "/namespaces/default/pods" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + now := time.Now() + created = &now + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + return nil, nil + } + }), + } + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + if err != nil { + t.Fatal(err) + } + resources, err := c.Build(objBody(&podList), false) + if err != nil { + t.Fatal(err) + } + + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + + if err != nil { + t.Fatal(err) + } + if len(result.Created) != 3 { + t.Errorf("expected 3 resource created, got %d", len(result.Created)) + } + + if err := c.Wait(resources, time.Second*30); err != nil { + t.Errorf("expected wait without error, got %s", err) + } + + if time.Since(*created) < time.Second*5 { + t.Errorf("expected to wait at least 5 seconds before ready status was detected, but got %s", time.Since(*created)) + } +} + +func TestWaitJob(t *testing.T) { + job := newJob("starfish", 0, intToInt32(1), 0, 0) + + var created *time.Time + + c := newTestClient(t) + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + switch { + case p == "/apis/batch/v1/namespaces/default/jobs/starfish" && m == http.MethodGet: + if created != nil && time.Since(*created) >= time.Second*5 { + job.Status.Succeeded = 1 + } + return newResponse(http.StatusOK, job) + case p == "/namespaces/default/jobs" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + now := time.Now() + created = &now + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + return nil, nil + } + }), + } + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + if err != nil { + t.Fatal(err) + } + resources, err := c.Build(objBody(job), false) + if err != nil { + t.Fatal(err) + } + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + + if err != nil { + t.Fatal(err) + } + if len(result.Created) != 1 { + t.Errorf("expected 1 resource created, got %d", len(result.Created)) + } + + if err := c.WaitWithJobs(resources, time.Second*30); err != nil { + t.Errorf("expected wait without error, got %s", err) + } + + if time.Since(*created) < time.Second*5 { + t.Errorf("expected to wait at least 5 seconds before ready status was detected, but got %s", time.Since(*created)) + } +} + +func TestWaitDelete(t *testing.T) { + pod := newPod("starfish") + + var deleted *time.Time + + c := newTestClient(t) + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + switch { + case p == "/namespaces/default/pods/starfish" && m == http.MethodGet: + if deleted != nil && time.Since(*deleted) >= time.Second*5 { + return newResponse(http.StatusNotFound, notFoundBody()) + } + return newResponse(http.StatusOK, &pod) + case p == "/namespaces/default/pods/starfish" && m == http.MethodDelete: + now := time.Now() + deleted = &now + return newResponse(http.StatusOK, &pod) + case p == "/namespaces/default/pods" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + return nil, nil + } + }), + } + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + if err != nil { + t.Fatal(err) + } + resources, err := c.Build(objBody(&pod), false) + if err != nil { + t.Fatal(err) + } + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + if err != nil { + t.Fatal(err) + } + if len(result.Created) != 1 { + t.Errorf("expected 1 resource created, got %d", len(result.Created)) + } + if _, err := c.Delete(resources, metav1.DeletePropagationBackground); err != nil { + t.Fatal(err) + } + + if err := c.WaitForDelete(resources, time.Second*30); err != nil { + t.Errorf("expected wait without error, got %s", err) + } + + if time.Since(*deleted) < time.Second*5 { + t.Errorf("expected to wait at least 5 seconds before ready status was detected, but got %s", time.Since(*deleted)) + } +} + func TestReal(t *testing.T) { t.Skip("This is a live test, comment this line to run") c := New(nil) @@ -348,7 +890,7 @@ func TestReal(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := c.Create(resources, CreateOptions{}); err != nil { + if _, err := c.Create(resources); err != nil { t.Fatal(err) } @@ -358,7 +900,7 @@ func TestReal(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err := c.Create(resources, CreateOptions{}); err != nil { + if _, err := c.Create(resources); err != nil { t.Fatal(err) } @@ -367,7 +909,7 @@ func TestReal(t *testing.T) { t.Fatal(err) } - if _, errs := c.Delete(resources, DeleteOptions{}); errs != nil { + if _, errs := c.Delete(resources, metav1.DeletePropagationBackground); errs != nil { t.Fatal(errs) } @@ -376,11 +918,42 @@ func TestReal(t *testing.T) { t.Fatal(err) } // ensures that delete does not fail if a resource is not found - if _, errs := c.Delete(resources, DeleteOptions{}); errs != nil { + if _, errs := c.Delete(resources, metav1.DeletePropagationBackground); errs != nil { t.Fatal(errs) } } +func TestGetPodList(t *testing.T) { + namespace := "some-namespace" + names := []string{"dave", "jimmy"} + var responsePodList v1.PodList + for _, name := range names { + responsePodList.Items = append(responsePodList.Items, newPodWithStatus(name, v1.PodStatus{}, namespace)) + } + + kubeClient := k8sfake.NewClientset(&responsePodList) + c := Client{Namespace: namespace, kubeClient: kubeClient} + + podList, err := c.GetPodList(namespace, metav1.ListOptions{}) + clientAssertions := assert.New(t) + clientAssertions.NoError(err) + clientAssertions.Equal(&responsePodList, podList) +} + +func TestOutputContainerLogsForPodList(t *testing.T) { + namespace := "some-namespace" + somePodList := newPodList("jimmy", "three", "structs") + + kubeClient := k8sfake.NewClientset(&somePodList) + c := Client{Namespace: namespace, kubeClient: kubeClient} + outBuffer := &bytes.Buffer{} + outBufferFunc := func(_, _, _ string) io.Writer { return outBuffer } + err := c.OutputContainerLogsForPodList(&somePodList, namespace, outBufferFunc) + clientAssertions := assert.New(t) + clientAssertions.NoError(err) + clientAssertions.Equal("fake logsfake logsfake logs", outBuffer.String()) +} + const testServiceManifest = ` kind: Service apiVersion: v1 @@ -451,11 +1024,11 @@ spec: apiVersion: v1 kind: Service metadata: - name: redis-slave + name: redis-replica labels: app: redis tier: backend - role: slave + role: replica spec: ports: # the port that this service should serve on @@ -463,24 +1036,24 @@ spec: selector: app: redis tier: backend - role: slave + role: replica --- apiVersion: extensions/v1beta1 kind: Deployment metadata: - name: redis-slave + name: redis-replica spec: replicas: 2 template: metadata: labels: app: redis - role: slave + role: replica tier: backend spec: containers: - - name: slave - image: gcr.io/google_samples/gb-redisslave:v1 + - name: replica + image: gcr.io/google_samples/gb-redisreplica:v1 resources: requests: cpu: 100m @@ -558,3 +1131,1194 @@ spec: ports: - containerPort: 80 ` + +var resourceQuotaConflict = []byte(` +{"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Operation cannot be fulfilled on resourcequotas \"quota\": the object has been modified; please apply your changes to the latest version and try again","reason":"Conflict","details":{"name":"quota","kind":"resourcequotas"},"code":409}`) + +type createPatchTestCase struct { + name string + + // The target state. + target *unstructured.Unstructured + // The state as it exists in the release. + original *unstructured.Unstructured + // The actual state as it exists in the cluster. + actual *unstructured.Unstructured + + threeWayMergeForUnstructured bool + // The patch is supposed to transfer the current state to the target state, + // thereby preserving the actual state, wherever possible. + expectedPatch string + expectedPatchType types.PatchType +} + +func (c createPatchTestCase) run(t *testing.T) { + scheme := runtime.NewScheme() + v1.AddToScheme(scheme) + encoder := jsonserializer.NewSerializerWithOptions( + jsonserializer.DefaultMetaFactory, scheme, scheme, jsonserializer.SerializerOptions{ + Yaml: false, Pretty: false, Strict: true, + }, + ) + objBody := func(obj runtime.Object) io.ReadCloser { + return io.NopCloser(bytes.NewReader([]byte(runtime.EncodeOrDie(encoder, obj)))) + } + header := make(http.Header) + header.Set("Content-Type", runtime.ContentTypeJSON) + restClient := &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Resp: &http.Response{ + StatusCode: http.StatusOK, + Body: objBody(c.actual), + Header: header, + }, + } + + targetInfo := &resource.Info{ + Client: restClient, + Namespace: "default", + Name: "test-obj", + Object: c.target, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{ + Group: "crd.com", + Version: "v1", + Resource: "datas", + }, + Scope: meta.RESTScopeNamespace, + }, + } + + patch, patchType, err := createPatch(c.original, targetInfo, c.threeWayMergeForUnstructured) + if err != nil { + t.Fatalf("Failed to create patch: %v", err) + } + + if c.expectedPatch != string(patch) { + t.Errorf("Unexpected patch.\nTarget:\n%s\nOriginal:\n%s\nActual:\n%s\n\nExpected:\n%s\nGot:\n%s", + c.target, + c.original, + c.actual, + c.expectedPatch, + string(patch), + ) + } + + if patchType != types.MergePatchType { + t.Errorf("Expected patch type %s, got %s", types.MergePatchType, patchType) + } +} + +func newTestCustomResourceData(metadata map[string]string, spec map[string]any) *unstructured.Unstructured { + if metadata == nil { + metadata = make(map[string]string) + } + if _, ok := metadata["name"]; !ok { + metadata["name"] = "test-obj" + } + if _, ok := metadata["namespace"]; !ok { + metadata["namespace"] = "default" + } + o := map[string]any{ + "apiVersion": "crd.com/v1", + "kind": "Data", + "metadata": metadata, + } + if len(spec) > 0 { + o["spec"] = spec + } + return &unstructured.Unstructured{ + Object: o, + } +} + +func TestCreatePatchCustomResourceMetadata(t *testing.T) { + target := newTestCustomResourceData(map[string]string{ + "meta.helm.sh/release-name": "foo-simple", + "meta.helm.sh/release-namespace": "default", + "objectset.rio.cattle.io/id": "default-foo-simple", + }, nil) + testCase := createPatchTestCase{ + name: "take ownership of resource", + target: target, + original: target, + actual: newTestCustomResourceData(nil, map[string]any{ + "color": "red", + }), + threeWayMergeForUnstructured: true, + expectedPatch: `{"metadata":{"meta.helm.sh/release-name":"foo-simple","meta.helm.sh/release-namespace":"default","objectset.rio.cattle.io/id":"default-foo-simple"}}`, + expectedPatchType: types.MergePatchType, + } + t.Run(testCase.name, testCase.run) + + // Previous behavior. + testCase.threeWayMergeForUnstructured = false + testCase.expectedPatch = `{}` + t.Run(testCase.name, testCase.run) +} + +func TestCreatePatchCustomResourceSpec(t *testing.T) { + target := newTestCustomResourceData(nil, map[string]any{ + "color": "red", + "size": "large", + }) + testCase := createPatchTestCase{ + name: "merge with spec of existing custom resource", + target: target, + original: target, + actual: newTestCustomResourceData(nil, map[string]any{ + "color": "red", + "weight": "heavy", + }), + threeWayMergeForUnstructured: true, + expectedPatch: `{"spec":{"size":"large"}}`, + expectedPatchType: types.MergePatchType, + } + t.Run(testCase.name, testCase.run) + + // Previous behavior. + testCase.threeWayMergeForUnstructured = false + testCase.expectedPatch = `{}` + t.Run(testCase.name, testCase.run) +} + +type errorFactory struct { + *cmdtesting.TestFactory + err error +} + +func (f *errorFactory) KubernetesClientSet() (*kubernetes.Clientset, error) { + return nil, f.err +} + +func newTestClientWithDiscoveryError(t *testing.T, err error) *Client { + t.Helper() + c := newTestClient(t) + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + if req.URL.Path == "/version" { + return nil, err + } + resp, respErr := newResponse(http.StatusOK, &v1.Pod{}) + return resp, respErr + }), + } + return c +} + +func TestIsReachable(t *testing.T) { + const ( + expectedUnreachableMsg = "kubernetes cluster unreachable" + ) + tests := []struct { + name string + setupClient func(*testing.T) *Client + expectError bool + errorContains string + }{ + { + name: "successful reachability test", + setupClient: func(t *testing.T) *Client { + t.Helper() + client := newTestClient(t) + client.kubeClient = k8sfake.NewClientset() + return client + }, + expectError: false, + }, + { + name: "client creation error with ErrEmptyConfig", + setupClient: func(t *testing.T) *Client { + t.Helper() + client := newTestClient(t) + client.Factory = &errorFactory{err: genericclioptions.ErrEmptyConfig} + return client + }, + expectError: true, + errorContains: expectedUnreachableMsg, + }, + { + name: "client creation error with general error", + setupClient: func(t *testing.T) *Client { + t.Helper() + client := newTestClient(t) + client.Factory = &errorFactory{err: errors.New("connection refused")} + return client + }, + expectError: true, + errorContains: "kubernetes cluster unreachable: connection refused", + }, + { + name: "discovery error with cluster unreachable", + setupClient: func(t *testing.T) *Client { + t.Helper() + return newTestClientWithDiscoveryError(t, http.ErrServerClosed) + }, + expectError: true, + errorContains: expectedUnreachableMsg, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client := tt.setupClient(t) + err := client.IsReachable() + + if tt.expectError { + if err == nil { + t.Error("expected error but got nil") + return + } + + if !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("expected error message to contain '%s', got: %v", tt.errorContains, err) + } + + } else { + if err != nil { + t.Errorf("expected no error but got: %v", err) + } + } + }) + } +} + +func TestIsIncompatibleServerError(t *testing.T) { + testCases := map[string]struct { + Err error + Want bool + }{ + "Unsupported media type": { + Err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusUnsupportedMediaType}}, + Want: true, + }, + "Not found error": { + Err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusNotFound}}, + Want: false, + }, + "Generic error": { + Err: fmt.Errorf("some generic error"), + Want: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + if got := isIncompatibleServerError(tc.Err); got != tc.Want { + t.Errorf("isIncompatibleServerError() = %v, want %v", got, tc.Want) + } + }) + } +} + +func TestReplaceResource(t *testing.T) { + type testCase struct { + Pods v1.PodList + Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) + ExpectedErrorContains string + } + + testCases := map[string]testCase{ + "normal": { + Pods: newPodList("whale"), + Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + switch len(previous) { + case 0: + assert.Equal(t, "GET", req.Method) + case 1: + assert.Equal(t, "PUT", req.Method) + } + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + }, + "conflict": { + Pods: newPodList("whale"), + Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + return &http.Response{ + StatusCode: http.StatusConflict, + Request: req, + }, nil + }, + ExpectedErrorContains: "failed to replace object: the server reported a conflict", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + + testFactory := cmdtesting.NewTestFactory() + t.Cleanup(testFactory.Cleanup) + + client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + return tc.Callback(t, tc, previous, req) + }) + + testFactory.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(client.Do), + } + + resourceList, err := buildResourceList(testFactory, v1.NamespaceDefault, FieldValidationDirectiveStrict, objBody(&tc.Pods), nil) + require.NoError(t, err) + + require.Len(t, resourceList, 1) + info := resourceList[0] + + err = replaceResource(info, FieldValidationDirectiveStrict) + if tc.ExpectedErrorContains != "" { + require.ErrorContains(t, err, tc.ExpectedErrorContains) + } else { + require.NoError(t, err) + require.NotNil(t, info.Object) + } + }) + } +} + +func TestPatchResourceClientSide(t *testing.T) { + type testCase struct { + OriginalPods v1.PodList + TargetPods v1.PodList + ThreeWayMergeForUnstructured bool + Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) + ExpectedErrorContains string + } + + testCases := map[string]testCase{ + "normal": { + OriginalPods: newPodList("whale"), + TargetPods: func() v1.PodList { + pods := newPodList("whale") + pods.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return pods + }(), + ThreeWayMergeForUnstructured: false, + Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + switch len(previous) { + case 0: + assert.Equal(t, "GET", req.Method) + return newResponse(http.StatusOK, &tc.OriginalPods.Items[0]) + case 1: + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/strategic-merge-patch+json", req.Header.Get("Content-Type")) + return newResponse(http.StatusOK, &tc.TargetPods.Items[0]) + } + + t.Fail() + return nil, nil + }, + }, + "three way merge for unstructured": { + OriginalPods: newPodList("whale"), + TargetPods: func() v1.PodList { + pods := newPodList("whale") + pods.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return pods + }(), + ThreeWayMergeForUnstructured: true, + Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + switch len(previous) { + case 0: + assert.Equal(t, "GET", req.Method) + return newResponse(http.StatusOK, &tc.OriginalPods.Items[0]) + case 1: + t.Logf("patcher: %+v", req.Header) + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/strategic-merge-patch+json", req.Header.Get("Content-Type")) + return newResponse(http.StatusOK, &tc.TargetPods.Items[0]) + } + + t.Fail() + return nil, nil + }, + }, + "conflict": { + OriginalPods: newPodList("whale"), + TargetPods: func() v1.PodList { + pods := newPodList("whale") + pods.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return pods + }(), + Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + switch len(previous) { + case 0: + assert.Equal(t, "GET", req.Method) + return newResponse(http.StatusOK, &tc.OriginalPods.Items[0]) + case 1: + assert.Equal(t, "PATCH", req.Method) + return &http.Response{ + StatusCode: http.StatusConflict, + Request: req, + }, nil + } + + t.Fail() + return nil, nil + + }, + ExpectedErrorContains: "cannot patch \"whale\" with kind Pod: the server reported a conflict", + }, + "no patch": { + OriginalPods: newPodList("whale"), + TargetPods: newPodList("whale"), + Callback: func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + switch len(previous) { + case 0: + assert.Equal(t, "GET", req.Method) + return newResponse(http.StatusOK, &tc.OriginalPods.Items[0]) + case 1: + assert.Equal(t, "GET", req.Method) + return newResponse(http.StatusOK, &tc.TargetPods.Items[0]) + } + + t.Fail() + return nil, nil // newResponse(http.StatusOK, &tc.TargetPods.Items[0]) + + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + + testFactory := cmdtesting.NewTestFactory() + t.Cleanup(testFactory.Cleanup) + + client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + return tc.Callback(t, tc, previous, req) + }) + + testFactory.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(client.Do), + } + + resourceListOriginal, err := buildResourceList(testFactory, v1.NamespaceDefault, FieldValidationDirectiveStrict, objBody(&tc.OriginalPods), nil) + require.NoError(t, err) + require.Len(t, resourceListOriginal, 1) + + resourceListTarget, err := buildResourceList(testFactory, v1.NamespaceDefault, FieldValidationDirectiveStrict, objBody(&tc.TargetPods), nil) + require.NoError(t, err) + require.Len(t, resourceListTarget, 1) + + original := resourceListOriginal[0] + target := resourceListTarget[0] + + err = patchResourceClientSide(original.Object, target, tc.ThreeWayMergeForUnstructured) + if tc.ExpectedErrorContains != "" { + require.ErrorContains(t, err, tc.ExpectedErrorContains) + } else { + require.NoError(t, err) + require.NotNil(t, target.Object) + } + }) + } +} + +func TestPatchResourceServerSide(t *testing.T) { + type testCase struct { + Pods v1.PodList + DryRun bool + ForceConflicts bool + FieldValidationDirective FieldValidationDirective + Callback func(t *testing.T, tc testCase, previous []RequestResponseAction, req *http.Request) (*http.Response, error) + ExpectedErrorContains string + } + + testCases := map[string]testCase{ + "normal": { + Pods: newPodList("whale"), + DryRun: false, + ForceConflicts: false, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type")) + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + assert.Equal(t, "false", req.URL.Query().Get("force")) + assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation")) + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + }, + "dry run": { + Pods: newPodList("whale"), + DryRun: true, + ForceConflicts: false, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type")) + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + assert.Equal(t, "All", req.URL.Query().Get("dryRun")) + assert.Equal(t, "false", req.URL.Query().Get("force")) + assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation")) + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + }, + "force conflicts": { + Pods: newPodList("whale"), + DryRun: false, + ForceConflicts: true, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type")) + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + assert.Equal(t, "true", req.URL.Query().Get("force")) + assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation")) + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + }, + "dry run + force conflicts": { + Pods: newPodList("whale"), + DryRun: true, + ForceConflicts: true, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type")) + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + assert.Equal(t, "All", req.URL.Query().Get("dryRun")) + assert.Equal(t, "true", req.URL.Query().Get("force")) + assert.Equal(t, "Strict", req.URL.Query().Get("fieldValidation")) + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + }, + "field validation ignore": { + Pods: newPodList("whale"), + DryRun: false, + ForceConflicts: false, + FieldValidationDirective: FieldValidationDirectiveIgnore, + Callback: func(t *testing.T, tc testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + assert.Equal(t, "PATCH", req.Method) + assert.Equal(t, "application/apply-patch+yaml", req.Header.Get("Content-Type")) + assert.Equal(t, "/namespaces/default/pods/whale", req.URL.Path) + assert.Equal(t, "false", req.URL.Query().Get("force")) + assert.Equal(t, "Ignore", req.URL.Query().Get("fieldValidation")) + + return newResponse(http.StatusOK, &tc.Pods.Items[0]) + }, + }, + "incompatible server": { + Pods: newPodList("whale"), + DryRun: false, + ForceConflicts: false, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + return &http.Response{ + StatusCode: http.StatusUnsupportedMediaType, + Request: req, + }, nil + }, + ExpectedErrorContains: "server-side apply not available on the server:", + }, + "conflict": { + Pods: newPodList("whale"), + DryRun: false, + ForceConflicts: false, + FieldValidationDirective: FieldValidationDirectiveStrict, + Callback: func(t *testing.T, _ testCase, _ []RequestResponseAction, req *http.Request) (*http.Response, error) { + t.Helper() + + return &http.Response{ + StatusCode: http.StatusConflict, + Request: req, + }, nil + }, + ExpectedErrorContains: "the server reported a conflict", + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + + testFactory := cmdtesting.NewTestFactory() + t.Cleanup(testFactory.Cleanup) + + client := NewRequestResponseLogClient(t, func(previous []RequestResponseAction, req *http.Request) (*http.Response, error) { + return tc.Callback(t, tc, previous, req) + }) + + testFactory.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(client.Do), + } + + resourceList, err := buildResourceList(testFactory, v1.NamespaceDefault, tc.FieldValidationDirective, objBody(&tc.Pods), nil) + require.NoError(t, err) + + require.Len(t, resourceList, 1) + info := resourceList[0] + + err = patchResourceServerSide(info, tc.DryRun, tc.ForceConflicts, tc.FieldValidationDirective) + if tc.ExpectedErrorContains != "" { + require.ErrorContains(t, err, tc.ExpectedErrorContains) + } else { + require.NoError(t, err) + require.NotNil(t, info.Object) + } + }) + } +} + +func TestDetermineFieldValidationDirective(t *testing.T) { + + assert.Equal(t, FieldValidationDirectiveIgnore, determineFieldValidationDirective(false)) + assert.Equal(t, FieldValidationDirectiveStrict, determineFieldValidationDirective(true)) +} + +func TestClientWaitContextCancellationLegacy(t *testing.T) { + podList := newPodList("starfish", "otter") + + ctx, cancel := context.WithCancel(t.Context()) + + c := newTestClient(t) + c.WaitContext = ctx + + requestCount := 0 + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + requestCount++ + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + + if requestCount == 2 { + cancel() + } + + switch { + case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet: + pod := &podList.Items[0] + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionFalse, + }, + } + return newResponse(http.StatusOK, pod) + case p == "/api/v1/namespaces/default/pods/otter" && m == http.MethodGet: + pod := &podList.Items[1] + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionFalse, + }, + } + return newResponse(http.StatusOK, pod) + case p == "/namespaces/default/pods" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Logf("unexpected request: %s %s", req.Method, req.URL.Path) + return newResponse(http.StatusNotFound, notFoundBody()) + } + }), + } + + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + require.NoError(t, err) + + resources, err := c.Build(objBody(&podList), false) + require.NoError(t, err) + + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + require.NoError(t, err) + assert.Len(t, result.Created, 2, "expected 2 resources created, got %d", len(result.Created)) + + err = c.Wait(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +func TestClientWaitWithJobsContextCancellationLegacy(t *testing.T) { + job := newJob("starfish", 0, intToInt32(1), 0, 0) + + ctx, cancel := context.WithCancel(t.Context()) + + c := newTestClient(t) + c.WaitContext = ctx + + requestCount := 0 + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + requestCount++ + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + + if requestCount == 2 { + cancel() + } + + switch { + case p == "/apis/batch/v1/namespaces/default/jobs/starfish" && m == http.MethodGet: + job.Status.Succeeded = 0 + return newResponse(http.StatusOK, job) + case p == "/namespaces/default/jobs" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Logf("unexpected request: %s %s", req.Method, req.URL.Path) + return newResponse(http.StatusNotFound, notFoundBody()) + } + }), + } + + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + require.NoError(t, err) + + resources, err := c.Build(objBody(job), false) + require.NoError(t, err) + + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + require.NoError(t, err) + assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) + + err = c.WaitWithJobs(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +func TestClientWaitForDeleteContextCancellationLegacy(t *testing.T) { + pod := newPod("starfish") + + ctx, cancel := context.WithCancel(t.Context()) + + c := newTestClient(t) + c.WaitContext = ctx + + deleted := false + requestCount := 0 + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + requestCount++ + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + + if requestCount == 3 { + cancel() + } + + switch { + case p == "/namespaces/default/pods/starfish" && m == http.MethodGet: + if deleted { + return newResponse(http.StatusOK, &pod) + } + return newResponse(http.StatusOK, &pod) + case p == "/namespaces/default/pods/starfish" && m == http.MethodDelete: + deleted = true + return newResponse(http.StatusOK, &pod) + case p == "/namespaces/default/pods" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Logf("unexpected request: %s %s", req.Method, req.URL.Path) + return newResponse(http.StatusNotFound, notFoundBody()) + } + }), + } + + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + require.NoError(t, err) + + resources, err := c.Build(objBody(&pod), false) + require.NoError(t, err) + + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + require.NoError(t, err) + assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) + + if _, err := c.Delete(resources, metav1.DeletePropagationBackground); err != nil { + t.Fatal(err) + } + + err = c.WaitForDelete(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +func TestClientWaitContextNilDoesNotPanic(t *testing.T) { + podList := newPodList("starfish") + + var created *time.Time + + c := newTestClient(t) + c.WaitContext = nil + + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + switch { + case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet: + pod := &podList.Items[0] + if created != nil && time.Since(*created) >= time.Second*2 { + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + } + } + return newResponse(http.StatusOK, pod) + case p == "/namespaces/default/pods" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + now := time.Now() + created = &now + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + return nil, nil + } + }), + } + + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + require.NoError(t, err) + + resources, err := c.Build(objBody(&podList), false) + require.NoError(t, err) + + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + require.NoError(t, err) + assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) + + err = c.Wait(resources, time.Second*30) + require.NoError(t, err) + + assert.GreaterOrEqual(t, time.Since(*created), time.Second*2, "expected to wait at least 2 seconds") +} + +func TestClientWaitContextPreCancelledLegacy(t *testing.T) { + podList := newPodList("starfish") + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + c := newTestClient(t) + c.WaitContext = ctx + + c.Factory.(*cmdtesting.TestFactory).Client = &fake.RESTClient{ + NegotiatedSerializer: unstructuredSerializer, + Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + p, m := req.URL.Path, req.Method + t.Logf("got request %s %s", p, m) + switch { + case p == "/api/v1/namespaces/default/pods/starfish" && m == http.MethodGet: + pod := &podList.Items[0] + return newResponse(http.StatusOK, pod) + case p == "/namespaces/default/pods" && m == http.MethodPost: + resources, err := c.Build(req.Body, false) + if err != nil { + t.Fatal(err) + } + return newResponse(http.StatusOK, resources[0].Object) + default: + t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) + return nil, nil + } + }), + } + + var err error + c.Waiter, err = c.GetWaiterWithOptions(LegacyStrategy) + require.NoError(t, err) + + resources, err := c.Build(objBody(&podList), false) + require.NoError(t, err) + + result, err := c.Create( + resources, + ClientCreateOptionServerSideApply(false, false)) + require.NoError(t, err) + assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) + + err = c.Wait(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +func TestClientWaitContextCancellationStatusWatcher(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + + c := newTestClient(t) + c.WaitContext = ctx + + podManifest := ` +apiVersion: v1 +kind: Pod +metadata: + name: test-pod + namespace: default +` + var err error + c.Waiter, err = c.GetWaiterWithOptions(StatusWatcherStrategy) + require.NoError(t, err) + + resources, err := c.Build(strings.NewReader(podManifest), false) + require.NoError(t, err) + + cancel() + + err = c.Wait(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +func TestClientWaitWithJobsContextCancellationStatusWatcher(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + + c := newTestClient(t) + c.WaitContext = ctx + + jobManifest := ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test-job + namespace: default +` + var err error + c.Waiter, err = c.GetWaiterWithOptions(StatusWatcherStrategy) + require.NoError(t, err) + + resources, err := c.Build(strings.NewReader(jobManifest), false) + require.NoError(t, err) + + cancel() + + err = c.WaitWithJobs(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +func TestClientWaitForDeleteContextCancellationStatusWatcher(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + + c := newTestClient(t) + c.WaitContext = ctx + + podManifest := ` +apiVersion: v1 +kind: Pod +metadata: + name: test-pod + namespace: default +status: + conditions: + - type: Ready + status: "True" + phase: Running +` + var err error + c.Waiter, err = c.GetWaiterWithOptions(StatusWatcherStrategy) + require.NoError(t, err) + + resources, err := c.Build(strings.NewReader(podManifest), false) + require.NoError(t, err) + + cancel() + + err = c.WaitForDelete(resources, time.Second*30) + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled", "expected context canceled error, got: %v", err) +} + +// testStatusReader is a custom status reader for testing that returns a configurable status. +type testStatusReader struct { + supportedGK schema.GroupKind + status status.Status +} + +func (r *testStatusReader) Supports(gk schema.GroupKind) bool { + return gk == r.supportedGK +} + +func (r *testStatusReader) ReadStatus(_ context.Context, _ engine.ClusterReader, id object.ObjMetadata) (*event.ResourceStatus, error) { + return &event.ResourceStatus{ + Identifier: id, + Status: r.status, + Message: "test status reader", + }, nil +} + +func (r *testStatusReader) ReadStatusForObject(_ context.Context, _ engine.ClusterReader, u *unstructured.Unstructured) (*event.ResourceStatus, error) { + id := object.ObjMetadata{ + Namespace: u.GetNamespace(), + Name: u.GetName(), + GroupKind: u.GroupVersionKind().GroupKind(), + } + return &event.ResourceStatus{ + Identifier: id, + Status: r.status, + Message: "test status reader", + }, nil +} + +func TestClientStatusReadersPassedToStatusWaiter(t *testing.T) { + // This test verifies that Client.StatusReaders is correctly passed through + // to the statusWaiter when using the StatusWatcherStrategy. + // We use a custom status reader that immediately returns CurrentStatus for pods, + // which allows a pod without Ready condition to pass the wait. + podManifest := ` +apiVersion: v1 +kind: Pod +metadata: + name: test-pod + namespace: default +` + + c := newTestClient(t) + statusReaders := []engine.StatusReader{ + &testStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.CurrentStatus, + }, + } + + // Create a fake dynamic client with the pod resource + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper(v1.SchemeGroupVersion.WithKind("Pod")) + + // Create the pod in the fake client + createManifest(t, podManifest, fakeMapper, fakeClient) + + // Set up the waiter with the fake client and custom status readers + c.Waiter = &statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + readers: statusReaders, + } + + resources, err := c.Build(strings.NewReader(podManifest), false) + require.NoError(t, err) + + // The pod has no Ready condition, but our custom reader returns CurrentStatus, + // so the wait should succeed immediately without timeout. + err = c.Wait(resources, time.Second*3) + require.NoError(t, err) +} + +func TestClientStatusReadersWithWaitWithJobs(t *testing.T) { + // This test verifies that Client.StatusReaders is correctly passed through + // to the statusWaiter when using WaitWithJobs. + jobManifest := ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test-job + namespace: default +` + + c := newTestClient(t) + statusReaders := []engine.StatusReader{ + &testStatusReader{ + supportedGK: schema.GroupKind{Group: "batch", Kind: "Job"}, + status: status.CurrentStatus, + }, + } + + // Create a fake dynamic client with the job resource + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper(batchv1.SchemeGroupVersion.WithKind("Job")) + + // Create the job in the fake client + createManifest(t, jobManifest, fakeMapper, fakeClient) + + // Set up the waiter with the fake client and custom status readers + c.Waiter = &statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + readers: statusReaders, + } + + resources, err := c.Build(strings.NewReader(jobManifest), false) + require.NoError(t, err) + + // The job has no Complete condition, but our custom reader returns CurrentStatus, + // so the wait should succeed immediately without timeout. + err = c.WaitWithJobs(resources, time.Second*3) + require.NoError(t, err) +} + +func createManifest(t *testing.T, manifest string, + fakeMapper meta.RESTMapper, fakeClient *dynamicfake.FakeDynamicClient) { + t.Helper() + + m := make(map[string]any) + err := yaml.Unmarshal([]byte(manifest), &m) + require.NoError(t, err) + obj := &unstructured.Unstructured{Object: m} + gvk := obj.GroupVersionKind() + mapping, err := fakeMapper.RESTMapping(gvk.GroupKind(), gvk.Version) + require.NoError(t, err) + err = fakeClient.Tracker().Create(mapping.Resource, obj, obj.GetNamespace()) + require.NoError(t, err) +} diff --git a/pkg/helm/pkg/kube/config.go b/pkg/helm/pkg/kube/config.go deleted file mode 100644 index e00c9acb..00000000 --- a/pkg/helm/pkg/kube/config.go +++ /dev/null @@ -1,30 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package kube // import "helm.sh/helm/v3/pkg/kube" - -import "k8s.io/cli-runtime/pkg/genericclioptions" - -// GetConfig returns a Kubernetes client config. -// -// Deprecated -func GetConfig(kubeconfig, context, namespace string) *genericclioptions.ConfigFlags { - cf := genericclioptions.NewConfigFlags(true) - cf.Namespace = &namespace - cf.Context = &context - cf.KubeConfig = &kubeconfig - return cf -} diff --git a/pkg/helm/pkg/kube/converter.go b/pkg/helm/pkg/kube/converter.go index 3bf0e358..5b1b7e13 100644 --- a/pkg/helm/pkg/kube/converter.go +++ b/pkg/helm/pkg/kube/converter.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "sync" diff --git a/pkg/helm/pkg/kube/extensions.go b/pkg/helm/pkg/kube/extensions.go deleted file mode 100644 index 52e86007..00000000 --- a/pkg/helm/pkg/kube/extensions.go +++ /dev/null @@ -1,36 +0,0 @@ -package kube - -import ( - "context" - "fmt" - - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func IsNotFound(err error) bool { - return err != nil && apierrors.IsNotFound(err) -} - -func (c *Client) DeleteNamespace(ctx context.Context, namespace string, opts DeleteOptions) error { - cs, err := c.Factory.KubernetesClientSet() - if err != nil { - return err - } - - if err := cs.CoreV1().Namespaces().Delete(ctx, namespace, metav1.DeleteOptions{}); err != nil { - return err - } - - if opts.Wait { - specs := []*ResourcesWaiterDeleteResourceSpec{ - {ResourceName: namespace, Namespace: "", GroupVersionResource: corev1.SchemeGroupVersion.WithResource("namespaces")}, - } - if err := c.ResourcesWaiter.WaitUntilDeleted(context.Background(), specs, opts.WaitTimeout); err != nil { - return fmt.Errorf("waiting until namespace deleted failed: %s", err) - } - } - - return nil -} diff --git a/pkg/helm/pkg/kube/factory.go b/pkg/helm/pkg/kube/factory.go index f19d62dc..92ae93f7 100644 --- a/pkg/helm/pkg/kube/factory.go +++ b/pkg/helm/pkg/kube/factory.go @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" "k8s.io/kubectl/pkg/validation" ) @@ -33,6 +34,9 @@ import ( // Helm does not need are not impacted or exposed. This minimizes the impact of Kubernetes changes // being exposed. type Factory interface { + // ToRESTConfig returns restconfig + ToRESTConfig() (*rest.Config, error) + // ToRawKubeConfigLoader return kubeconfig loader as-is ToRawKubeConfigLoader() clientcmd.ClientConfig diff --git a/pkg/helm/pkg/kube/fake/extensions.go b/pkg/helm/pkg/kube/fake/extensions.go deleted file mode 100644 index 8f67b13c..00000000 --- a/pkg/helm/pkg/kube/fake/extensions.go +++ /dev/null @@ -1,11 +0,0 @@ -package fake - -import ( - "context" - - "github.com/werf/nelm/pkg/helm/pkg/kube" -) - -func (c *PrintingKubeClient) DeleteNamespace(ctx context.Context, namespace string, opts kube.DeleteOptions) error { - return nil -} diff --git a/pkg/helm/pkg/kube/fake/failing_kube_client.go b/pkg/helm/pkg/kube/fake/failing_kube_client.go new file mode 100644 index 00000000..8440468f --- /dev/null +++ b/pkg/helm/pkg/kube/fake/failing_kube_client.go @@ -0,0 +1,189 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package fake implements various fake KubeClients for use in testing +package fake + +import ( + "io" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/resource" + + "github.com/werf/nelm/pkg/helm/pkg/kube" +) + +// FailingKubeClient implements KubeClient for testing purposes. It also has +// additional errors you can set to fail different functions, otherwise it +// delegates all its calls to `PrintingKubeClient` +type FailingKubeClient struct { + PrintingKubeClient + CreateError error + GetError error + DeleteError error + UpdateError error + BuildError error + BuildTableError error + ConnectionError error + BuildDummy bool + DummyResources kube.ResourceList + BuildUnstructuredError error + WaitError error + WaitForDeleteError error + WatchUntilReadyError error + WaitDuration time.Duration + // RecordedWaitOptions stores the WaitOptions passed to GetWaiter for testing + RecordedWaitOptions []kube.WaitOption +} + +var _ kube.Interface = &FailingKubeClient{} + +// FailingKubeWaiter implements kube.Waiter for testing purposes. +// It also has additional errors you can set to fail different functions, otherwise it delegates all its calls to `PrintingKubeWaiter` +type FailingKubeWaiter struct { + *PrintingKubeWaiter + waitError error + waitForDeleteError error + watchUntilReadyError error + waitDuration time.Duration +} + +// Create returns the configured error if set or prints +func (f *FailingKubeClient) Create(resources kube.ResourceList, options ...kube.ClientCreateOption) (*kube.Result, error) { + if f.CreateError != nil { + return nil, f.CreateError + } + return f.PrintingKubeClient.Create(resources, options...) +} + +// Get returns the configured error if set or prints +func (f *FailingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) { + if f.GetError != nil { + return nil, f.GetError + } + return f.PrintingKubeClient.Get(resources, related) +} + +// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. +func (f *FailingKubeWaiter) Wait(resources kube.ResourceList, d time.Duration) error { + time.Sleep(f.waitDuration) + if f.waitError != nil { + return f.waitError + } + return f.PrintingKubeWaiter.Wait(resources, d) +} + +// WaitWithJobs returns the configured error if set or prints +func (f *FailingKubeWaiter) WaitWithJobs(resources kube.ResourceList, d time.Duration) error { + if f.waitError != nil { + return f.waitError + } + return f.PrintingKubeWaiter.WaitWithJobs(resources, d) +} + +// WaitForDelete returns the configured error if set or prints +func (f *FailingKubeWaiter) WaitForDelete(resources kube.ResourceList, d time.Duration) error { + if f.waitForDeleteError != nil { + return f.waitForDeleteError + } + return f.PrintingKubeWaiter.WaitForDelete(resources, d) +} + +// Delete returns the configured error if set or prints +func (f *FailingKubeClient) Delete(resources kube.ResourceList, deletionPropagation metav1.DeletionPropagation) (*kube.Result, []error) { + if f.DeleteError != nil { + return nil, []error{f.DeleteError} + } + + return f.PrintingKubeClient.Delete(resources, deletionPropagation) +} + +// WatchUntilReady returns the configured error if set or prints +func (f *FailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, d time.Duration) error { + if f.watchUntilReadyError != nil { + return f.watchUntilReadyError + } + return f.PrintingKubeWaiter.WatchUntilReady(resources, d) +} + +// Update returns the configured error if set or prints +func (f *FailingKubeClient) Update(r, modified kube.ResourceList, options ...kube.ClientUpdateOption) (*kube.Result, error) { + if f.UpdateError != nil { + return &kube.Result{}, f.UpdateError + } + return f.PrintingKubeClient.Update(r, modified, options...) +} + +// Build returns the configured error if set or prints +func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error) { + if f.BuildError != nil { + return []*resource.Info{}, f.BuildError + } + if f.DummyResources != nil { + return f.DummyResources, nil + } + if f.BuildDummy { + return createDummyResourceList(), nil + } + return f.PrintingKubeClient.Build(r, false) +} + +// BuildTable returns the configured error if set or prints +func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList, error) { + if f.BuildTableError != nil { + return []*resource.Info{}, f.BuildTableError + } + if f.BuildDummy { + return createDummyResourceList(), nil + } + return f.PrintingKubeClient.BuildTable(r, false) +} + +func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) { + return f.GetWaiterWithOptions(ws) +} + +func (f *FailingKubeClient) GetWaiterWithOptions(ws kube.WaitStrategy, opts ...kube.WaitOption) (kube.Waiter, error) { + // Record the WaitOptions for testing + f.RecordedWaitOptions = append(f.RecordedWaitOptions, opts...) + waiter, _ := f.PrintingKubeClient.GetWaiterWithOptions(ws, opts...) + printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter) + return &FailingKubeWaiter{ + PrintingKubeWaiter: printingKubeWaiter, + waitError: f.WaitError, + waitForDeleteError: f.WaitForDeleteError, + watchUntilReadyError: f.WatchUntilReadyError, + waitDuration: f.WaitDuration, + }, nil +} + +func (f *FailingKubeClient) IsReachable() error { + if f.ConnectionError != nil { + return f.ConnectionError + } + return f.PrintingKubeClient.IsReachable() +} + +func createDummyResourceList() kube.ResourceList { + var resInfo resource.Info + resInfo.Name = "dummyName" + resInfo.Namespace = "dummyNamespace" + var resourceList kube.ResourceList + resourceList.Append(&resInfo) + return resourceList +} diff --git a/pkg/helm/pkg/kube/fake/fake.go b/pkg/helm/pkg/kube/fake/fake.go deleted file mode 100644 index 581d8bef..00000000 --- a/pkg/helm/pkg/kube/fake/fake.go +++ /dev/null @@ -1,160 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package fake implements various fake KubeClients for use in testing -package fake - -import ( - "io" - "time" - - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/resource" - - "github.com/werf/nelm/pkg/helm/pkg/kube" -) - -// FailingKubeClient implements KubeClient for testing purposes. It also has -// additional errors you can set to fail different functions, otherwise it -// delegates all its calls to `PrintingKubeClient` -type FailingKubeClient struct { - PrintingKubeClient - CreateError error - GetError error - WaitError error - DeleteError error - DeleteWithPropagationError error - WatchUntilReadyError error - UpdateError error - BuildError error - BuildTableError error - BuildDummy bool - BuildUnstructuredError error - WaitAndGetCompletedPodPhaseError error - WaitDuration time.Duration -} - -// Create returns the configured error if set or prints -func (f *FailingKubeClient) Create(resources kube.ResourceList, opts kube.CreateOptions) (*kube.Result, error) { - if f.CreateError != nil { - return nil, f.CreateError - } - return f.PrintingKubeClient.Create(resources, opts) -} - -// Get returns the configured error if set or prints -func (f *FailingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) { - if f.GetError != nil { - return nil, f.GetError - } - return f.PrintingKubeClient.Get(resources, related) -} - -// Waits the amount of time defined on f.WaitDuration, then returns the configured error if set or prints. -func (f *FailingKubeClient) Wait(resources kube.ResourceList, d time.Duration) error { - time.Sleep(f.WaitDuration) - if f.WaitError != nil { - return f.WaitError - } - return f.PrintingKubeClient.Wait(resources, d) -} - -// WaitWithJobs returns the configured error if set or prints -func (f *FailingKubeClient) WaitWithJobs(resources kube.ResourceList, d time.Duration) error { - if f.WaitError != nil { - return f.WaitError - } - return f.PrintingKubeClient.WaitWithJobs(resources, d) -} - -// WaitForDelete returns the configured error if set or prints -func (f *FailingKubeClient) WaitForDelete(resources kube.ResourceList, d time.Duration) error { - if f.WaitError != nil { - return f.WaitError - } - return f.PrintingKubeClient.WaitForDelete(resources, d) -} - -// Delete returns the configured error if set or prints -func (f *FailingKubeClient) Delete(resources kube.ResourceList, opts kube.DeleteOptions) (*kube.Result, []error) { - if f.DeleteError != nil { - return nil, []error{f.DeleteError} - } - return f.PrintingKubeClient.Delete(resources, opts) -} - -// WatchUntilReady returns the configured error if set or prints -func (f *FailingKubeClient) WatchUntilReady(resources kube.ResourceList, d time.Duration) error { - if f.WatchUntilReadyError != nil { - return f.WatchUntilReadyError - } - return f.PrintingKubeClient.WatchUntilReady(resources, d) -} - -// Update returns the configured error if set or prints -func (f *FailingKubeClient) Update(r, modified kube.ResourceList, force bool, opts kube.UpdateOptions) (*kube.Result, error) { - if f.UpdateError != nil { - return &kube.Result{}, f.UpdateError - } - return f.PrintingKubeClient.Update(r, modified, force, opts) -} - -// Build returns the configured error if set or prints -func (f *FailingKubeClient) Build(r io.Reader, _ bool) (kube.ResourceList, error) { - if f.BuildError != nil { - return []*resource.Info{}, f.BuildError - } - if f.BuildDummy { - return createDummyResourceList(), nil - } - return f.PrintingKubeClient.Build(r, false) -} - -// BuildTable returns the configured error if set or prints -func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList, error) { - if f.BuildTableError != nil { - return []*resource.Info{}, f.BuildTableError - } - return f.PrintingKubeClient.BuildTable(r, false) -} - -// WaitAndGetCompletedPodPhase returns the configured error if set or prints -func (f *FailingKubeClient) WaitAndGetCompletedPodPhase(s string, d time.Duration) (v1.PodPhase, error) { - if f.WaitAndGetCompletedPodPhaseError != nil { - return v1.PodSucceeded, f.WaitAndGetCompletedPodPhaseError - } - return f.PrintingKubeClient.WaitAndGetCompletedPodPhase(s, d) -} - -// DeleteWithPropagationPolicy returns the configured error if set or prints -func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation, opts kube.DeleteOptions) (*kube.Result, []error) { - if f.DeleteWithPropagationError != nil { - return nil, []error{f.DeleteWithPropagationError} - } - return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy, opts) -} - -func createDummyResourceList() kube.ResourceList { - var resInfo resource.Info - resInfo.Name = "dummyName" - resInfo.Namespace = "dummyNamespace" - var resourceList kube.ResourceList - resourceList.Append(&resInfo) - return resourceList - -} diff --git a/pkg/helm/pkg/kube/fake/printer.go b/pkg/helm/pkg/kube/fake/printer.go index 298d3158..7e0c98b4 100644 --- a/pkg/helm/pkg/kube/fake/printer.go +++ b/pkg/helm/pkg/kube/fake/printer.go @@ -17,6 +17,7 @@ limitations under the License. package fake import ( + "fmt" "io" "strings" "time" @@ -32,16 +33,25 @@ import ( // PrintingKubeClient implements KubeClient, but simply prints the reader to // the given output. type PrintingKubeClient struct { - Out io.Writer + Out io.Writer + LogOutput io.Writer } +// PrintingKubeWaiter implements kube.Waiter, but simply prints the reader to the given output +type PrintingKubeWaiter struct { + Out io.Writer + LogOutput io.Writer +} + +var _ kube.Interface = &PrintingKubeClient{} + // IsReachable checks if the cluster is reachable func (p *PrintingKubeClient) IsReachable() error { return nil } // Create prints the values of what would be created with a real KubeClient. -func (p *PrintingKubeClient) Create(resources kube.ResourceList, _ kube.CreateOptions) (*kube.Result, error) { +func (p *PrintingKubeClient) Create(resources kube.ResourceList, _ ...kube.ClientCreateOption) (*kube.Result, error) { _, err := io.Copy(p.Out, bufferize(resources)) if err != nil { return nil, err @@ -57,17 +67,23 @@ func (p *PrintingKubeClient) Get(resources kube.ResourceList, _ bool) (map[strin return make(map[string][]runtime.Object), nil } -func (p *PrintingKubeClient) Wait(resources kube.ResourceList, _ time.Duration) error { +func (p *PrintingKubeWaiter) Wait(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err } -func (p *PrintingKubeClient) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error { +func (p *PrintingKubeWaiter) WaitWithJobs(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err } -func (p *PrintingKubeClient) WaitForDelete(resources kube.ResourceList, _ time.Duration) error { +func (p *PrintingKubeWaiter) WaitForDelete(resources kube.ResourceList, _ time.Duration) error { + _, err := io.Copy(p.Out, bufferize(resources)) + return err +} + +// WatchUntilReady implements KubeClient WatchUntilReady. +func (p *PrintingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ time.Duration) error { _, err := io.Copy(p.Out, bufferize(resources)) return err } @@ -75,7 +91,7 @@ func (p *PrintingKubeClient) WaitForDelete(resources kube.ResourceList, _ time.D // Delete implements KubeClient delete. // // It only prints out the content to be deleted. -func (p *PrintingKubeClient) Delete(resources kube.ResourceList, _ kube.DeleteOptions) (*kube.Result, []error) { +func (p *PrintingKubeClient) Delete(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) { _, err := io.Copy(p.Out, bufferize(resources)) if err != nil { return nil, []error{err} @@ -83,14 +99,8 @@ func (p *PrintingKubeClient) Delete(resources kube.ResourceList, _ kube.DeleteOp return &kube.Result{Deleted: resources}, nil } -// WatchUntilReady implements KubeClient WatchUntilReady. -func (p *PrintingKubeClient) WatchUntilReady(resources kube.ResourceList, _ time.Duration) error { - _, err := io.Copy(p.Out, bufferize(resources)) - return err -} - // Update implements KubeClient Update. -func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, force bool, _ kube.UpdateOptions) (*kube.Result, error) { +func (p *PrintingKubeClient) Update(_, modified kube.ResourceList, _ ...kube.ClientUpdateOption) (*kube.Result, error) { _, err := io.Copy(p.Out, bufferize(modified)) if err != nil { return nil, err @@ -116,10 +126,21 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati return v1.PodSucceeded, nil } +// GetPodList implements KubeClient GetPodList. +func (p *PrintingKubeClient) GetPodList(_ string, _ metav1.ListOptions) (*v1.PodList, error) { + return &v1.PodList{}, nil +} + +// OutputContainerLogsForPodList implements KubeClient OutputContainerLogsForPodList. +func (p *PrintingKubeClient) OutputContainerLogsForPodList(_ *v1.PodList, someNamespace string, _ func(namespace, pod, container string) io.Writer) error { + _, err := io.Copy(p.LogOutput, strings.NewReader(fmt.Sprintf("attempted to output logs for namespace: %s", someNamespace))) + return err +} + // DeleteWithPropagationPolicy implements KubeClient delete. // // It only prints out the content to be deleted. -func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation, _ kube.DeleteOptions) (*kube.Result, []error) { +func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) { _, err := io.Copy(p.Out, bufferize(resources)) if err != nil { return nil, []error{err} @@ -127,6 +148,14 @@ func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.Resource return &kube.Result{Deleted: resources}, nil } +func (p *PrintingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) { + return p.GetWaiterWithOptions(ws) +} + +func (p *PrintingKubeClient) GetWaiterWithOptions(_ kube.WaitStrategy, _ ...kube.WaitOption) (kube.Waiter, error) { + return &PrintingKubeWaiter{Out: p.Out, LogOutput: p.LogOutput}, nil +} + func bufferize(resources kube.ResourceList) io.Reader { var builder strings.Builder for _, info := range resources { diff --git a/pkg/helm/pkg/kube/interface.go b/pkg/helm/pkg/kube/interface.go index 74681b0a..63c78475 100644 --- a/pkg/helm/pkg/kube/interface.go +++ b/pkg/helm/pkg/kube/interface.go @@ -17,7 +17,6 @@ limitations under the License. package kube import ( - "context" "io" "time" @@ -30,33 +29,22 @@ import ( // // A KubernetesClient must be concurrency safe. type Interface interface { - // Create creates one or more resources. - Create(resources ResourceList, opts CreateOptions) (*Result, error) - - // Wait waits up to the given timeout for the specified resources to be ready. - Wait(resources ResourceList, timeout time.Duration) error + // Get details of deployed resources. + // The first argument is a list of resources to get. The second argument + // specifies if related pods should be fetched. For example, the pods being + // managed by a deployment. + Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) - // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. - WaitWithJobs(resources ResourceList, timeout time.Duration) error + // Create creates one or more resources. + Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) - // Delete destroys one or more resources. - Delete(resources ResourceList, opts DeleteOptions) (*Result, []error) - DeleteNamespace(ctx context.Context, namespace string, opts DeleteOptions) error - - // WatchUntilReady watches the resources given and waits until it is ready. - // - // This method is mainly for hook implementations. It watches for a resource to - // hit a particular milestone. The milestone depends on the Kind. - // - // For Jobs, "ready" means the Job ran to completion (exited without error). - // For Pods, "ready" means the Pod phase is marked "succeeded". - // For all other kinds, it means the kind was created or modified without - // error. - WatchUntilReady(resources ResourceList, timeout time.Duration) error + // Delete destroys one or more resources using the specified deletion propagation policy. + // The 'policy' parameter determines how child resources are handled during deletion. + Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) // Update updates one or more resources or creates the resource // if it doesn't exist. - Update(original, target ResourceList, force bool, opts UpdateOptions) (*Result, error) + Update(original, target ResourceList, options ...ClientUpdateOption) (*Result, error) // Build creates a resource list from a Reader. // @@ -65,40 +53,17 @@ type Interface interface { // // Validates against OpenAPI schema if validate is true. Build(reader io.Reader, validate bool) (ResourceList, error) - - // WaitAndGetCompletedPodPhase waits up to a timeout until a pod enters a completed phase - // and returns said phase (PodSucceeded or PodFailed qualify). - WaitAndGetCompletedPodPhase(name string, timeout time.Duration) (v1.PodPhase, error) - // IsReachable checks whether the client is able to connect to the cluster. IsReachable() error -} -// InterfaceExt is introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceExt and integrate its method(s) into the Interface. -type InterfaceExt interface { - // WaitForDelete wait up to the given timeout for the specified resources to be deleted. - WaitForDelete(resources ResourceList, timeout time.Duration) error -} + // GetWaiter gets the Kube.Waiter. + GetWaiter(ws WaitStrategy) (Waiter, error) -// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface. -type InterfaceDeletionPropagation interface { - // Delete destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value. - DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation, opts DeleteOptions) (*Result, []error) -} + // GetPodList lists all pods that match the specified listOptions + GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) -// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface. -type InterfaceResources interface { - // Get details of deployed resources. - // The first argument is a list of resources to get. The second argument - // specifies if related pods should be fetched. For example, the pods being - // managed by a deployment. - Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) + // OutputContainerLogsForPodList outputs the logs for a pod list + OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error // BuildTable creates a resource list from a Reader. This differs from // Interface.Build() in that a table kind is returned. A table is useful @@ -112,25 +77,36 @@ type InterfaceResources interface { BuildTable(reader io.Reader, validate bool) (ResourceList, error) } -var _ Interface = (*Client)(nil) -var _ InterfaceExt = (*Client)(nil) -var _ InterfaceDeletionPropagation = (*Client)(nil) -var _ InterfaceResources = (*Client)(nil) +// Waiter defines methods related to waiting for resource states. +type Waiter interface { + // Wait waits up to the given timeout for the specified resources to be ready. + Wait(resources ResourceList, timeout time.Duration) error -type CreateOptions struct { - SkipIfAlreadyExists bool -} + // WaitWithJobs wait up to the given timeout for the specified resources to be ready, including jobs. + WaitWithJobs(resources ResourceList, timeout time.Duration) error -type UpdateOptions struct { - SkipDeleteIfInvalidOwnership bool - ReleaseName string // Required if SkipDeleteIfInvalidOwnership == true - ReleaseNamespace string // Required if SkipDeleteIfInvalidOwnership == true + // WaitForDelete wait up to the given timeout for the specified resources to be deleted. + WaitForDelete(resources ResourceList, timeout time.Duration) error + + // WatchUntilReady watches the resources given and waits until it is ready. + // + // This method is mainly for hook implementations. It watches for a resource to + // hit a particular milestone. The milestone depends on the Kind. + // + // For Jobs, "ready" means the Job ran to completion (exited without error). + // For Pods, "ready" means the Pod phase is marked "succeeded". + // For all other kinds, it means the kind was created or modified without + // error. + WatchUntilReady(resources ResourceList, timeout time.Duration) error } -type DeleteOptions struct { - Wait bool - WaitTimeout time.Duration - SkipIfInvalidOwnership bool - ReleaseName string // Required if SkipIfInvalidOwnership == true - ReleaseNamespace string // Required if SkipIfInvalidOwnership == true +// InterfaceWaitOptions defines an interface that extends Interface with +// methods that accept wait options. +// +// TODO Helm 5: Remove InterfaceWaitOptions and integrate its method(s) into the Interface. +type InterfaceWaitOptions interface { + // GetWaiter gets the Kube.Waiter with options. + GetWaiterWithOptions(ws WaitStrategy, opts ...WaitOption) (Waiter, error) } + +var _ InterfaceWaitOptions = (*Client)(nil) diff --git a/pkg/helm/pkg/kube/options.go b/pkg/helm/pkg/kube/options.go new file mode 100644 index 00000000..3326c284 --- /dev/null +++ b/pkg/helm/pkg/kube/options.go @@ -0,0 +1,82 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "context" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" +) + +// WaitOption is a function that configures an option for waiting on resources. +type WaitOption func(*waitOptions) + +// WithWaitContext sets the context for waiting on resources. +// If unset, context.Background() will be used. +func WithWaitContext(ctx context.Context) WaitOption { + return func(wo *waitOptions) { + wo.ctx = ctx + } +} + +// WithWatchUntilReadyMethodContext sets the context specifically for the WatchUntilReady method. +// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`). +func WithWatchUntilReadyMethodContext(ctx context.Context) WaitOption { + return func(wo *waitOptions) { + wo.watchUntilReadyCtx = ctx + } +} + +// WithWaitMethodContext sets the context specifically for the Wait method. +// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`). +func WithWaitMethodContext(ctx context.Context) WaitOption { + return func(wo *waitOptions) { + wo.waitCtx = ctx + } +} + +// WithWaitWithJobsMethodContext sets the context specifically for the WaitWithJobs method. +// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`). +func WithWaitWithJobsMethodContext(ctx context.Context) WaitOption { + return func(wo *waitOptions) { + wo.waitWithJobsCtx = ctx + } +} + +// WithWaitForDeleteMethodContext sets the context specifically for the WaitForDelete method. +// If unset, the context set by `WithWaitContext` will be used (falling back to `context.Background()`). +func WithWaitForDeleteMethodContext(ctx context.Context) WaitOption { + return func(wo *waitOptions) { + wo.waitForDeleteCtx = ctx + } +} + +// WithKStatusReaders sets the status readers to be used while waiting on resources. +func WithKStatusReaders(readers ...engine.StatusReader) WaitOption { + return func(wo *waitOptions) { + wo.statusReaders = readers + } +} + +type waitOptions struct { + ctx context.Context + watchUntilReadyCtx context.Context + waitCtx context.Context + waitWithJobsCtx context.Context + waitForDeleteCtx context.Context + statusReaders []engine.StatusReader +} diff --git a/pkg/helm/pkg/kube/ready.go b/pkg/helm/pkg/kube/ready.go index cdf5c0cf..734015b1 100644 --- a/pkg/helm/pkg/kube/ready.go +++ b/pkg/helm/pkg/kube/ready.go @@ -14,18 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "context" "fmt" + "log/slog" appsv1 "k8s.io/api/apps/v1" - appsv1beta1 "k8s.io/api/apps/v1beta1" - appsv1beta2 "k8s.io/api/apps/v1beta2" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" - extensionsv1beta1 "k8s.io/api/extensions/v1beta1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -60,13 +58,9 @@ func CheckJobs(checkJobs bool) ReadyCheckerOption { // NewReadyChecker creates a new checker. Passed ReadyCheckerOptions can // be used to override defaults. -func NewReadyChecker(cl kubernetes.Interface, log func(string, ...interface{}), opts ...ReadyCheckerOption) ReadyChecker { +func NewReadyChecker(cl kubernetes.Interface, opts ...ReadyCheckerOption) ReadyChecker { c := ReadyChecker{ client: cl, - log: log, - } - if c.log == nil { - c.log = nopLogger } for _, opt := range opts { opt(&c) @@ -77,7 +71,6 @@ func NewReadyChecker(cl kubernetes.Interface, log func(string, ...interface{}), // ReadyChecker is a type that can check core Kubernetes types for readiness. type ReadyChecker struct { client kubernetes.Interface - log func(string, ...interface{}) checkJobs bool pausedAsReady bool } @@ -105,7 +98,7 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err ready, err := c.jobReady(job) return ready, err } - case *appsv1.Deployment, *appsv1beta1.Deployment, *appsv1beta2.Deployment, *extensionsv1beta1.Deployment: + case *appsv1.Deployment: currentDeployment, err := c.client.AppsV1().Deployments(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) if err != nil { return false, err @@ -138,7 +131,7 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err if !c.serviceReady(svc) { return false, nil } - case *extensionsv1beta1.DaemonSet, *appsv1.DaemonSet, *appsv1beta2.DaemonSet: + case *appsv1.DaemonSet: ds, err := c.client.AppsV1().DaemonSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) if err != nil { return false, err @@ -168,7 +161,7 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err if !c.crdReady(*crd) { return false, nil } - case *appsv1.StatefulSet, *appsv1beta1.StatefulSet, *appsv1beta2.StatefulSet: + case *appsv1.StatefulSet: sts, err := c.client.AppsV1().StatefulSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) if err != nil { return false, err @@ -188,7 +181,7 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err if !ready || err != nil { return false, err } - case *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet: + case *appsv1.ReplicaSet: rs, err := c.client.AppsV1().ReplicaSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{}) if err != nil { return false, err @@ -233,20 +226,21 @@ func (c *ReadyChecker) isPodReady(pod *corev1.Pod) bool { return true } } - c.log("Pod is not ready: %s/%s", pod.GetNamespace(), pod.GetName()) + slog.Debug("Pod is not ready", "namespace", pod.GetNamespace(), "name", pod.GetName()) return false } func (c *ReadyChecker) jobReady(job *batchv1.Job) (bool, error) { if job.Status.Failed > *job.Spec.BackoffLimit { - c.log("Job is failed: %s/%s", job.GetNamespace(), job.GetName()) + slog.Debug("Job is failed", "namespace", job.GetNamespace(), "name", job.GetName()) // If a job is failed, it can't recover, so throw an error return false, fmt.Errorf("job is failed: %s/%s", job.GetNamespace(), job.GetName()) } if job.Spec.Completions != nil && job.Status.Succeeded < *job.Spec.Completions { - c.log("Job is not completed: %s/%s", job.GetNamespace(), job.GetName()) + slog.Debug("Job is not completed", "namespace", job.GetNamespace(), "name", job.GetName()) return false, nil } + slog.Debug("Job is completed", "namespace", job.GetNamespace(), "name", job.GetName()) return true, nil } @@ -258,7 +252,7 @@ func (c *ReadyChecker) serviceReady(s *corev1.Service) bool { // Ensure that the service cluster IP is not empty if s.Spec.ClusterIP == "" { - c.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName()) + slog.Debug("Service does not have cluster IP address", "namespace", s.GetNamespace(), "name", s.GetName()) return false } @@ -266,24 +260,25 @@ func (c *ReadyChecker) serviceReady(s *corev1.Service) bool { if s.Spec.Type == corev1.ServiceTypeLoadBalancer { // do not wait when at least 1 external IP is set if len(s.Spec.ExternalIPs) > 0 { - c.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs) + slog.Debug("Service has external IP addresses", "namespace", s.GetNamespace(), "name", s.GetName(), "externalIPs", s.Spec.ExternalIPs) return true } if s.Status.LoadBalancer.Ingress == nil { - c.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName()) + slog.Debug("Service does not have load balancer ingress IP address", "namespace", s.GetNamespace(), "name", s.GetName()) return false } } - + slog.Debug("Service is ready", "namespace", s.GetNamespace(), "name", s.GetName(), "clusterIP", s.Spec.ClusterIP, "externalIPs", s.Spec.ExternalIPs) return true } func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool { if v.Status.Phase != corev1.ClaimBound { - c.log("PersistentVolumeClaim is not bound: %s/%s", v.GetNamespace(), v.GetName()) + slog.Debug("PersistentVolumeClaim is not bound", "namespace", v.GetNamespace(), "name", v.GetName()) return false } + slog.Debug("PersistentVolumeClaim is bound", "namespace", v.GetNamespace(), "name", v.GetName(), "phase", v.Status.Phase) return true } @@ -293,23 +288,24 @@ func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deploy return false } // Verify the generation observed by the deployment controller matches the spec generation - if dep.Status.ObservedGeneration != dep.ObjectMeta.Generation { - c.log("Deployment is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", dep.Namespace, dep.Name, dep.Status.ObservedGeneration, dep.ObjectMeta.Generation) + if dep.Status.ObservedGeneration != dep.Generation { + slog.Debug("Deployment is not ready, observedGeneration does not match spec generation", "namespace", dep.GetNamespace(), "name", dep.GetName(), "actualGeneration", dep.Status.ObservedGeneration, "expectedGeneration", dep.Generation) return false } expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep) - if !(rs.Status.ReadyReplicas >= expectedReady) { - c.log("Deployment is not ready: %s/%s. %d out of %d expected pods are ready", dep.Namespace, dep.Name, rs.Status.ReadyReplicas, expectedReady) + if rs.Status.ReadyReplicas < expectedReady { + slog.Debug("Deployment does not have enough pods ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady) return false } + slog.Debug("Deployment is ready", "namespace", dep.GetNamespace(), "name", dep.GetName(), "readyPods", rs.Status.ReadyReplicas, "totalPods", expectedReady) return true } func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool { // Verify the generation observed by the daemonSet controller matches the spec generation - if ds.Status.ObservedGeneration != ds.ObjectMeta.Generation { - c.log("DaemonSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", ds.Namespace, ds.Name, ds.Status.ObservedGeneration, ds.ObjectMeta.Generation) + if ds.Status.ObservedGeneration != ds.Generation { + slog.Debug("DaemonSet is not ready, observedGeneration does not match spec generation", "namespace", ds.GetNamespace(), "name", ds.GetName(), "observedGeneration", ds.Status.ObservedGeneration, "expectedGeneration", ds.Generation) return false } @@ -320,7 +316,7 @@ func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool { // Make sure all the updated pods have been scheduled if ds.Status.UpdatedNumberScheduled != ds.Status.DesiredNumberScheduled { - c.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", ds.Namespace, ds.Name, ds.Status.UpdatedNumberScheduled, ds.Status.DesiredNumberScheduled) + slog.Debug("DaemonSet does not have enough Pods scheduled", "namespace", ds.GetNamespace(), "name", ds.GetName(), "scheduledPods", ds.Status.UpdatedNumberScheduled, "totalPods", ds.Status.DesiredNumberScheduled) return false } maxUnavailable, err := intstr.GetScaledValueFromIntOrPercent(ds.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable, int(ds.Status.DesiredNumberScheduled), true) @@ -332,10 +328,11 @@ func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool { } expectedReady := int(ds.Status.DesiredNumberScheduled) - maxUnavailable - if !(int(ds.Status.NumberReady) >= expectedReady) { - c.log("DaemonSet is not ready: %s/%s. %d out of %d expected pods are ready", ds.Namespace, ds.Name, ds.Status.NumberReady, expectedReady) + if int(ds.Status.NumberReady) < expectedReady { + slog.Debug("DaemonSet does not have enough Pods ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady) return false } + slog.Debug("DaemonSet is ready", "namespace", ds.GetNamespace(), "name", ds.GetName(), "readyPods", ds.Status.NumberReady, "totalPods", expectedReady) return true } @@ -357,6 +354,8 @@ func (c *ReadyChecker) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) // continue. return true } + default: + // intentionally left empty } } return false @@ -377,6 +376,8 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool { // continue. return true } + default: + // intentionally left empty } } return false @@ -384,14 +385,14 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool { func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool { // Verify the generation observed by the statefulSet controller matches the spec generation - if sts.Status.ObservedGeneration != sts.ObjectMeta.Generation { - c.log("StatefulSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", sts.Namespace, sts.Name, sts.Status.ObservedGeneration, sts.ObjectMeta.Generation) + if sts.Status.ObservedGeneration != sts.Generation { + slog.Debug("StatefulSet is not ready, observedGeneration doest not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation) return false } // If the update strategy is not a rolling update, there will be nothing to wait for if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType { - c.log("StatefulSet skipped ready check: %s/%s. updateStrategy is %v", sts.Namespace, sts.Name, sts.Spec.UpdateStrategy.Type) + slog.Debug("StatefulSet skipped ready check", "namespace", sts.GetNamespace(), "name", sts.GetName(), "updateStrategy", sts.Spec.UpdateStrategy.Type) return true } @@ -417,30 +418,29 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool { // Make sure all the updated pods have been scheduled if int(sts.Status.UpdatedReplicas) < expectedReplicas { - c.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods have been scheduled", sts.Namespace, sts.Name, sts.Status.UpdatedReplicas, expectedReplicas) + slog.Debug("StatefulSet does not have enough Pods scheduled", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.UpdatedReplicas, "totalPods", expectedReplicas) return false } if int(sts.Status.ReadyReplicas) != replicas { - c.log("StatefulSet is not ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas) + slog.Debug("StatefulSet does not have enough Pods ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas) return false } // This check only makes sense when all partitions are being upgraded otherwise during a - // partioned rolling upgrade, this condition will never evaluate to true, leading to + // partitioned rolling upgrade, this condition will never evaluate to true, leading to // error. if partition == 0 && sts.Status.CurrentRevision != sts.Status.UpdateRevision { - c.log("StatefulSet is not ready: %s/%s. currentRevision %s does not yet match updateRevision %s", sts.Namespace, sts.Name, sts.Status.CurrentRevision, sts.Status.UpdateRevision) + slog.Debug("StatefulSet is not ready, currentRevision does not match updateRevision", "namespace", sts.GetNamespace(), "name", sts.GetName(), "currentRevision", sts.Status.CurrentRevision, "updateRevision", sts.Status.UpdateRevision) return false } - - c.log("StatefulSet is ready: %s/%s. %d out of %d expected pods are ready", sts.Namespace, sts.Name, sts.Status.ReadyReplicas, replicas) + slog.Debug("StatefulSet is ready", "namespace", sts.GetNamespace(), "name", sts.GetName(), "readyPods", sts.Status.ReadyReplicas, "totalPods", replicas) return true } func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool { // Verify the generation observed by the replicationController controller matches the spec generation - if rc.Status.ObservedGeneration != rc.ObjectMeta.Generation { - c.log("ReplicationController is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", rc.Namespace, rc.Name, rc.Status.ObservedGeneration, rc.ObjectMeta.Generation) + if rc.Status.ObservedGeneration != rc.Generation { + slog.Debug("ReplicationController is not ready, observedGeneration doest not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation) return false } return true @@ -448,8 +448,8 @@ func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationControll func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool { // Verify the generation observed by the replicaSet controller matches the spec generation - if rs.Status.ObservedGeneration != rs.ObjectMeta.Generation { - c.log("ReplicaSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", rs.Namespace, rs.Name, rs.Status.ObservedGeneration, rs.ObjectMeta.Generation) + if rs.Status.ObservedGeneration != rs.Generation { + slog.Debug("ReplicaSet is not ready, observedGeneration doest not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation) return false } return true @@ -459,5 +459,8 @@ func getPods(ctx context.Context, client kubernetes.Interface, namespace, select list, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: selector, }) - return list.Items, err + if err != nil { + return nil, fmt.Errorf("failed to list pods: %w", err) + } + return list.Items, nil } diff --git a/pkg/helm/pkg/kube/ready_test.go b/pkg/helm/pkg/kube/ready_test.go index 3b8c4b80..1fb0da5b 100644 --- a/pkg/helm/pkg/kube/ready_test.go +++ b/pkg/helm/pkg/kube/ready_test.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "context" @@ -22,14 +22,677 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) const defaultNamespace = metav1.NamespaceDefault +func Test_ReadyChecker_IsReady_Pod(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + pod *corev1.Pod + want bool + wantErr bool + }{ + { + name: "IsReady Pod", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.Pod{}, Name: "foo", Namespace: defaultNamespace}, + }, + pod: newPodWithCondition("foo", corev1.ConditionTrue), + want: true, + wantErr: false, + }, + { + name: "IsReady Pod returns error", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.Pod{}, Name: "foo", Namespace: defaultNamespace}, + }, + pod: newPodWithCondition("bar", corev1.ConditionTrue), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(t.Context(), tt.pod, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create Pod error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_Job(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + job *batchv1.Job + want bool + wantErr bool + }{ + { + name: "IsReady Job error while getting job", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &batchv1.Job{}, Name: "foo", Namespace: defaultNamespace}, + }, + job: newJob("bar", 1, intToInt32(1), 1, 0), + want: false, + wantErr: true, + }, + { + name: "IsReady Job", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &batchv1.Job{}, Name: "foo", Namespace: defaultNamespace}, + }, + job: newJob("foo", 1, intToInt32(1), 1, 0), + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.BatchV1().Jobs(defaultNamespace).Create(t.Context(), tt.job, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create Job error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_Deployment(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + replicaSet *appsv1.ReplicaSet + deployment *appsv1.Deployment + want bool + wantErr bool + }{ + { + name: "IsReady Deployments error while getting current Deployment", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.Deployment{}, Name: "foo", Namespace: defaultNamespace}, + }, + replicaSet: newReplicaSet("foo", 0, 0, true), + deployment: newDeployment("bar", 1, 1, 0, true), + want: false, + wantErr: true, + }, + { + name: "IsReady Deployments", //TODO fix this one + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.Deployment{}, Name: "foo", Namespace: defaultNamespace}, + }, + replicaSet: newReplicaSet("foo", 0, 0, true), + deployment: newDeployment("foo", 1, 1, 0, true), + want: false, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.AppsV1().Deployments(defaultNamespace).Create(t.Context(), tt.deployment, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create Deployment error: %v", err) + return + } + if _, err := c.client.AppsV1().ReplicaSets(defaultNamespace).Create(t.Context(), tt.replicaSet, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create ReplicaSet error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_PersistentVolumeClaim(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + pvc *corev1.PersistentVolumeClaim + want bool + wantErr bool + }{ + { + name: "IsReady PersistentVolumeClaim", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.PersistentVolumeClaim{}, Name: "foo", Namespace: defaultNamespace}, + }, + pvc: newPersistentVolumeClaim("foo", corev1.ClaimPending), + want: false, + wantErr: false, + }, + { + name: "IsReady PersistentVolumeClaim with error", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.PersistentVolumeClaim{}, Name: "foo", Namespace: defaultNamespace}, + }, + pvc: newPersistentVolumeClaim("bar", corev1.ClaimPending), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.CoreV1().PersistentVolumeClaims(defaultNamespace).Create(t.Context(), tt.pvc, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create PersistentVolumeClaim error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_Service(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + svc *corev1.Service + want bool + wantErr bool + }{ + { + name: "IsReady Service", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.Service{}, Name: "foo", Namespace: defaultNamespace}, + }, + svc: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, ClusterIP: ""}), + want: false, + wantErr: false, + }, + { + name: "IsReady Service with error", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.Service{}, Name: "foo", Namespace: defaultNamespace}, + }, + svc: newService("bar", corev1.ServiceSpec{Type: corev1.ServiceTypeExternalName, ClusterIP: ""}), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.CoreV1().Services(defaultNamespace).Create(t.Context(), tt.svc, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create Service error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_DaemonSet(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + ds *appsv1.DaemonSet + want bool + wantErr bool + }{ + { + name: "IsReady DaemonSet", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.DaemonSet{}, Name: "foo", Namespace: defaultNamespace}, + }, + ds: newDaemonSet("foo", 0, 0, 1, 0, true), + want: false, + wantErr: false, + }, + { + name: "IsReady DaemonSet with error", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.DaemonSet{}, Name: "foo", Namespace: defaultNamespace}, + }, + ds: newDaemonSet("bar", 0, 1, 1, 1, true), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.AppsV1().DaemonSets(defaultNamespace).Create(t.Context(), tt.ds, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create DaemonSet error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_StatefulSet(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + ss *appsv1.StatefulSet + want bool + wantErr bool + }{ + { + name: "IsReady StatefulSet", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.StatefulSet{}, Name: "foo", Namespace: defaultNamespace}, + }, + ss: newStatefulSet("foo", 1, 0, 0, 1, true), + want: false, + wantErr: false, + }, + { + name: "IsReady StatefulSet with error", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.StatefulSet{}, Name: "foo", Namespace: defaultNamespace}, + }, + ss: newStatefulSet("bar", 1, 0, 1, 1, true), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.AppsV1().StatefulSets(defaultNamespace).Create(t.Context(), tt.ss, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create StatefulSet error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_ReplicationController(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + rc *corev1.ReplicationController + want bool + wantErr bool + }{ + { + name: "IsReady ReplicationController", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.ReplicationController{}, Name: "foo", Namespace: defaultNamespace}, + }, + rc: newReplicationController("foo", false), + want: false, + wantErr: false, + }, + { + name: "IsReady ReplicationController with error", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.ReplicationController{}, Name: "foo", Namespace: defaultNamespace}, + }, + rc: newReplicationController("bar", false), + want: false, + wantErr: true, + }, + { + name: "IsReady ReplicationController and pods not ready for object", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &corev1.ReplicationController{}, Name: "foo", Namespace: defaultNamespace}, + }, + rc: newReplicationController("foo", true), + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + if _, err := c.client.CoreV1().ReplicationControllers(defaultNamespace).Create(t.Context(), tt.rc, metav1.CreateOptions{}); err != nil { + t.Errorf("Failed to create ReplicationController error: %v", err) + return + } + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_IsReady_ReplicaSet(t *testing.T) { + type fields struct { + client kubernetes.Interface + checkJobs bool + pausedAsReady bool + } + type args struct { + ctx context.Context + resource *resource.Info + } + tests := []struct { + name string + fields fields + args args + rs *appsv1.ReplicaSet + want bool + wantErr bool + }{ + { + name: "IsReady ReplicaSet", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.ReplicaSet{}, Name: "foo", Namespace: defaultNamespace}, + }, + rs: newReplicaSet("foo", 1, 1, true), + want: false, + wantErr: true, + }, + { + name: "IsReady ReplicaSet not ready", + fields: fields{ + client: fake.NewClientset(), + checkJobs: true, + pausedAsReady: false, + }, + args: args{ + ctx: t.Context(), + resource: &resource.Info{Object: &appsv1.ReplicaSet{}, Name: "foo", Namespace: defaultNamespace}, + }, + rs: newReplicaSet("bar", 1, 1, false), + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &ReadyChecker{ + client: tt.fields.client, + checkJobs: tt.fields.checkJobs, + pausedAsReady: tt.fields.pausedAsReady, + } + // + got, err := c.IsReady(tt.args.ctx, tt.args.resource) + if (err != nil) != tt.wantErr { + t.Errorf("IsReady() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("IsReady() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_ReadyChecker_deploymentReady(t *testing.T) { type args struct { rs *appsv1.ReplicaSet @@ -91,7 +754,7 @@ func Test_ReadyChecker_deploymentReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) if got := c.deploymentReady(tt.args.rs, tt.args.dep); got != tt.want { t.Errorf("deploymentReady() = %v, want %v", got, tt.want) } @@ -125,7 +788,7 @@ func Test_ReadyChecker_replicaSetReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) if got := c.replicaSetReady(tt.args.rs); got != tt.want { t.Errorf("replicaSetReady() = %v, want %v", got, tt.want) } @@ -159,7 +822,7 @@ func Test_ReadyChecker_replicationControllerReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) if got := c.replicationControllerReady(tt.args.rc); got != tt.want { t.Errorf("replicationControllerReady() = %v, want %v", got, tt.want) } @@ -214,7 +877,7 @@ func Test_ReadyChecker_daemonSetReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) if got := c.daemonSetReady(tt.args.ds); got != tt.want { t.Errorf("daemonSetReady() = %v, want %v", got, tt.want) } @@ -290,7 +953,7 @@ func Test_ReadyChecker_statefulSetReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) if got := c.statefulSetReady(tt.args.sts); got != tt.want { t.Errorf("statefulSetReady() = %v, want %v", got, tt.want) } @@ -334,17 +997,29 @@ func Test_ReadyChecker_podsReadyForObject(t *testing.T) { want: false, wantErr: false, }, + { + name: "ReplicaSet not set", + args: args{ + namespace: defaultNamespace, + obj: nil, + }, + existPods: []corev1.Pod{ + *newPodWithCondition("foo", corev1.ConditionFalse), + }, + want: false, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) for _, pod := range tt.existPods { - if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(context.TODO(), &pod, metav1.CreateOptions{}); err != nil { + if _, err := c.client.CoreV1().Pods(defaultNamespace).Create(t.Context(), &pod, metav1.CreateOptions{}); err != nil { t.Errorf("Failed to create Pod error: %v", err) return } } - got, err := c.podsReadyForObject(context.TODO(), tt.args.namespace, tt.args.obj) + got, err := c.podsReadyForObject(t.Context(), tt.args.namespace, tt.args.obj) if (err != nil) != tt.wantErr { t.Errorf("podsReadyForObject() error = %v, wantErr %v", err, tt.wantErr) return @@ -416,7 +1091,7 @@ func Test_ReadyChecker_jobReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) got, err := c.jobReady(tt.args.job) if (err != nil) != tt.wantErr { t.Errorf("jobReady() error = %v, wantErr %v", err, tt.wantErr) @@ -455,7 +1130,7 @@ func Test_ReadyChecker_volumeReady(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - c := NewReadyChecker(fake.NewSimpleClientset(), nil) + c := NewReadyChecker(fake.NewClientset()) if got := c.volumeReady(tt.args.v); got != tt.want { t.Errorf("volumeReady() = %v, want %v", got, tt.want) } @@ -463,6 +1138,190 @@ func Test_ReadyChecker_volumeReady(t *testing.T) { } } +func Test_ReadyChecker_serviceReady(t *testing.T) { + type args struct { + service *corev1.Service + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "service type is of external name", + args: args{service: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeExternalName, ClusterIP: ""})}, + want: true, + }, + { + name: "service cluster ip is empty", + args: args{service: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, ClusterIP: ""})}, + want: false, + }, + { + name: "service has a cluster ip that is greater than 0", + args: args{service: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, ClusterIP: "bar", ExternalIPs: []string{"bar"}})}, + want: true, + }, + { + name: "service has a cluster ip that is less than 0 and ingress is nil", + args: args{service: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeLoadBalancer, ClusterIP: "bar"})}, + want: false, + }, + { + name: "service has a cluster ip that is less than 0 and ingress is nil", + args: args{service: newService("foo", corev1.ServiceSpec{Type: corev1.ServiceTypeClusterIP, ClusterIP: "bar"})}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewReadyChecker(fake.NewClientset()) + got := c.serviceReady(tt.args.service) + if got != tt.want { + t.Errorf("serviceReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_crdBetaReady(t *testing.T) { + type args struct { + crdBeta apiextv1beta1.CustomResourceDefinition + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "crdBeta type is Establish and Conditional is true", + args: args{crdBeta: newcrdBetaReady("foo", apiextv1beta1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1beta1.CustomResourceDefinitionCondition{ + { + Type: apiextv1beta1.Established, + Status: apiextv1beta1.ConditionTrue, + }, + }, + })}, + want: true, + }, + { + name: "crdBeta type is Establish and Conditional is false", + args: args{crdBeta: newcrdBetaReady("foo", apiextv1beta1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1beta1.CustomResourceDefinitionCondition{ + { + Type: apiextv1beta1.Established, + Status: apiextv1beta1.ConditionFalse, + }, + }, + })}, + want: false, + }, + { + name: "crdBeta type is NamesAccepted and Conditional is true", + args: args{crdBeta: newcrdBetaReady("foo", apiextv1beta1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1beta1.CustomResourceDefinitionCondition{ + { + Type: apiextv1beta1.NamesAccepted, + Status: apiextv1beta1.ConditionTrue, + }, + }, + })}, + want: false, + }, + { + name: "crdBeta type is NamesAccepted and Conditional is false", + args: args{crdBeta: newcrdBetaReady("foo", apiextv1beta1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1beta1.CustomResourceDefinitionCondition{ + { + Type: apiextv1beta1.NamesAccepted, + Status: apiextv1beta1.ConditionFalse, + }, + }, + })}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewReadyChecker(fake.NewClientset()) + got := c.crdBetaReady(tt.args.crdBeta) + if got != tt.want { + t.Errorf("crdBetaReady() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ReadyChecker_crdReady(t *testing.T) { + type args struct { + crdBeta apiextv1.CustomResourceDefinition + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "crdBeta type is Establish and Conditional is true", + args: args{crdBeta: newcrdReady("foo", apiextv1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: apiextv1.Established, + Status: apiextv1.ConditionTrue, + }, + }, + })}, + want: true, + }, + { + name: "crdBeta type is Establish and Conditional is false", + args: args{crdBeta: newcrdReady("foo", apiextv1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: apiextv1.Established, + Status: apiextv1.ConditionFalse, + }, + }, + })}, + want: false, + }, + { + name: "crdBeta type is NamesAccepted and Conditional is true", + args: args{crdBeta: newcrdReady("foo", apiextv1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: apiextv1.NamesAccepted, + Status: apiextv1.ConditionTrue, + }, + }, + })}, + want: false, + }, + { + name: "crdBeta type is NamesAccepted and Conditional is false", + args: args{crdBeta: newcrdReady("foo", apiextv1.CustomResourceDefinitionStatus{ + Conditions: []apiextv1.CustomResourceDefinitionCondition{ + { + Type: apiextv1.NamesAccepted, + Status: apiextv1.ConditionFalse, + }, + }, + })}, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := NewReadyChecker(fake.NewClientset()) + got := c.crdReady(tt.args.crdBeta) + if got != tt.want { + t.Errorf("crdBetaReady() = %v, want %v", got, tt.want) + } + }) + } +} + func newStatefulSetWithUpdateRevision(name string, replicas, partition, readyReplicas, updatedReplicas int, updateRevision string, generationInSync bool) *appsv1.StatefulSet { ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas, generationInSync) ss.Status.UpdateRevision = updateRevision @@ -699,6 +1558,43 @@ func newJob(name string, backoffLimit int, completions *int32, succeeded int, fa } } +func newService(name string, serviceSpec corev1.ServiceSpec) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: serviceSpec, + Status: corev1.ServiceStatus{ + LoadBalancer: corev1.LoadBalancerStatus{ + Ingress: nil, + }, + }, + } +} + +func newcrdBetaReady(name string, crdBetaStatus apiextv1beta1.CustomResourceDefinitionStatus) apiextv1beta1.CustomResourceDefinition { + return apiextv1beta1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: apiextv1beta1.CustomResourceDefinitionSpec{}, + Status: crdBetaStatus, + } +} + +func newcrdReady(name string, crdBetaStatus apiextv1.CustomResourceDefinitionStatus) apiextv1.CustomResourceDefinition { + return apiextv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: defaultNamespace, + }, + Spec: apiextv1.CustomResourceDefinitionSpec{}, + Status: crdBetaStatus, + } +} + func intToInt32(i int) *int32 { i32 := int32(i) return &i32 diff --git a/pkg/helm/pkg/kube/resource.go b/pkg/helm/pkg/kube/resource.go index 0f9ae03a..555e3679 100644 --- a/pkg/helm/pkg/kube/resource.go +++ b/pkg/helm/pkg/kube/resource.go @@ -14,15 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" -import ( - "fmt" - - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/resource" -) +import "k8s.io/cli-runtime/pkg/resource" // ResourceList provides convenience methods for comparing collections of Infos. type ResourceList []*resource.Info @@ -32,7 +26,7 @@ func (r *ResourceList) Append(val *resource.Info) { *r = append(*r, val) } -// Visit implements resource.Visitor. +// Visit implements resource.Visitor. The visitor stops if fn returns an error. func (r ResourceList) Visit(fn resource.VisitorFunc) error { for _, i := range r { if err := fn(i, nil); err != nil { @@ -44,7 +38,7 @@ func (r ResourceList) Visit(fn resource.VisitorFunc) error { // Filter returns a new Result with Infos that satisfy the predicate fn. func (r ResourceList) Filter(fn func(*resource.Info) bool) ResourceList { - result := ResourceList{} + var result ResourceList for _, i := range r { if fn(i) { result.Append(i) @@ -85,39 +79,14 @@ func (r ResourceList) Intersect(rs ResourceList) ResourceList { return r.Filter(rs.Contains) } -// isMatchingInfo returns true if infos match on Name and GroupVersionKind. +// isMatchingInfo returns true if infos match on Name, Namespace, Group and Kind. +// +// IMPORTANT: Version is intentionally excluded from the comparison. Resources +// served by the same CRD at different API versions (e.g. v2beta1 vs v2beta2) +// share the same underlying storage in the Kubernetes API server. Comparing +// the full GroupVersionKind causes Difference() to treat a version change as +// a resource removal + addition, which makes Helm delete the resource it just +// created during upgrades. See https://github.com/helm/helm/issues/31768 func isMatchingInfo(a, b *resource.Info) bool { - return a.Name == b.Name && a.Namespace == b.Namespace && a.Mapping.GroupVersionKind.Kind == b.Mapping.GroupVersionKind.Kind -} - -func (r ResourceList) ToYamlDocs() (string, error) { - var manifestsStr string - for _, res := range r { - var err error - unstructuredObj := unstructured.Unstructured{} - unstructuredObj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(res.Object) - if err != nil { - return "", fmt.Errorf("error converting object to unstructured type: %w", err) - } - - objByte, err := unstructuredObj.MarshalJSON() - if err != nil { - return "", fmt.Errorf("error marshaling object: %w", err) - } - - manifestsStr = fmt.Sprintf("%s\n---\n%s", manifestsStr, string(objByte)) - } - - return manifestsStr, nil -} - -func (r *ResourceList) Merge(rs ResourceList) { - *r = r.Difference(rs) - for _, res := range rs { - r.Append(res) - } -} - -func ResourceNameNamespaceKind(info *resource.Info) string { - return fmt.Sprint(info.Namespace, ":", info.Object.GetObjectKind().GroupVersionKind().Kind, "/", info.Name) + return a.Name == b.Name && a.Namespace == b.Namespace && a.Mapping.GroupVersionKind.GroupKind() == b.Mapping.GroupVersionKind.GroupKind() } diff --git a/pkg/helm/pkg/kube/resource_policy.go b/pkg/helm/pkg/kube/resource_policy.go index 46b8680d..9bc8bd0f 100644 --- a/pkg/helm/pkg/kube/resource_policy.go +++ b/pkg/helm/pkg/kube/resource_policy.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" // ResourcePolicyAnno is the annotation name for a resource policy const ResourcePolicyAnno = "helm.sh/resource-policy" diff --git a/pkg/helm/pkg/kube/resource_test.go b/pkg/helm/pkg/kube/resource_test.go index 3c906cec..a37e3ecf 100644 --- a/pkg/helm/pkg/kube/resource_test.go +++ b/pkg/helm/pkg/kube/resource_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "testing" @@ -59,3 +59,42 @@ func TestResourceList(t *testing.T) { t.Error("expected intersect to return bar") } } + +func TestIsMatchingInfo(t *testing.T) { + gvk := schema.GroupVersionKind{Group: "group1", Version: "version1", Kind: "pod"} + resourceInfo := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvk}} + + gvkDiffGroup := schema.GroupVersionKind{Group: "diff", Version: "version1", Kind: "pod"} + resourceInfoDiffGroup := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkDiffGroup}} + if isMatchingInfo(&resourceInfo, &resourceInfoDiffGroup) { + t.Error("expected resources not equal") + } + + gvkDiffVersion := schema.GroupVersionKind{Group: "group1", Version: "diff", Kind: "pod"} + resourceInfoDiffVersion := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkDiffVersion}} + if !isMatchingInfo(&resourceInfo, &resourceInfoDiffVersion) { + t.Error("expected resources with different versions but same group and kind to be equal") + } + + gvkDiffKind := schema.GroupVersionKind{Group: "group1", Version: "version1", Kind: "deployment"} + resourceInfoDiffKind := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkDiffKind}} + if isMatchingInfo(&resourceInfo, &resourceInfoDiffKind) { + t.Error("expected resources not equal") + } + + resourceInfoDiffName := resource.Info{Name: "diff", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvk}} + if isMatchingInfo(&resourceInfo, &resourceInfoDiffName) { + t.Error("expected resources not equal") + } + + resourceInfoDiffNamespace := resource.Info{Name: "name1", Namespace: "diff", Mapping: &meta.RESTMapping{GroupVersionKind: gvk}} + if isMatchingInfo(&resourceInfo, &resourceInfoDiffNamespace) { + t.Error("expected resources not equal") + } + + gvkEqual := schema.GroupVersionKind{Group: "group1", Version: "version1", Kind: "pod"} + resourceInfoEqual := resource.Info{Name: "name1", Namespace: "namespace1", Mapping: &meta.RESTMapping{GroupVersionKind: gvkEqual}} + if !isMatchingInfo(&resourceInfo, &resourceInfoEqual) { + t.Error("expected resources to be equal") + } +} diff --git a/pkg/helm/pkg/kube/resources_waiter.go b/pkg/helm/pkg/kube/resources_waiter.go deleted file mode 100644 index 16929fd6..00000000 --- a/pkg/helm/pkg/kube/resources_waiter.go +++ /dev/null @@ -1,20 +0,0 @@ -package kube - -import ( - "context" - "time" - - "k8s.io/apimachinery/pkg/runtime/schema" -) - -type ResourcesWaiter interface { - Wait(ctx context.Context, resources ResourceList, timeout time.Duration) error - WatchUntilReady(ctx context.Context, resources ResourceList, timeout time.Duration) error - WaitUntilDeleted(ctx context.Context, specs []*ResourcesWaiterDeleteResourceSpec, timeout time.Duration) error -} - -type ResourcesWaiterDeleteResourceSpec struct { - ResourceName string - Namespace string - GroupVersionResource schema.GroupVersionResource -} diff --git a/pkg/helm/pkg/cli/roundtripper.go b/pkg/helm/pkg/kube/roundtripper.go similarity index 83% rename from pkg/helm/pkg/cli/roundtripper.go rename to pkg/helm/pkg/kube/roundtripper.go index 9cd4eacb..52cb5bad 100644 --- a/pkg/helm/pkg/cli/roundtripper.go +++ b/pkg/helm/pkg/kube/roundtripper.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cli +package kube import ( "bytes" @@ -24,19 +24,19 @@ import ( "strings" ) -type retryingRoundTripper struct { - wrapped http.RoundTripper +type RetryingRoundTripper struct { + Wrapped http.RoundTripper } -func (rt *retryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { +func (rt *RetryingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return rt.roundTrip(req, 1, nil) } -func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { +func (rt *RetryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp *http.Response) (*http.Response, error) { if retry < 0 { return prevResp, nil } - resp, rtErr := rt.wrapped.RoundTrip(req) + resp, rtErr := rt.Wrapped.RoundTrip(req) if rtErr != nil { return resp, rtErr } @@ -49,7 +49,7 @@ func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp b, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { - return resp, rtErr + return resp, err } var ke kubernetesError @@ -58,10 +58,10 @@ func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp r.Seek(0, io.SeekStart) resp.Body = io.NopCloser(r) if err != nil { - return resp, rtErr + return resp, err } if ke.Code < 500 { - return resp, rtErr + return resp, nil } // Matches messages like "etcdserver: leader changed" if strings.HasSuffix(ke.Message, "etcdserver: leader changed") { @@ -71,7 +71,7 @@ func (rt *retryingRoundTripper) roundTrip(req *http.Request, retry int, prevResp if strings.HasSuffix(ke.Message, "raft proposal dropped") { return rt.roundTrip(req, retry-1, resp) } - return resp, rtErr + return resp, nil } type kubernetesError struct { diff --git a/pkg/helm/pkg/kube/roundtripper_test.go b/pkg/helm/pkg/kube/roundtripper_test.go new file mode 100644 index 00000000..96602c1f --- /dev/null +++ b/pkg/helm/pkg/kube/roundtripper_test.go @@ -0,0 +1,161 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "encoding/json" + "errors" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +type fakeRoundTripper struct { + resp *http.Response + err error + calls int +} + +func (f *fakeRoundTripper) RoundTrip(_ *http.Request) (*http.Response, error) { + f.calls++ + return f.resp, f.err +} + +func newRespWithBody(statusCode int, contentType, body string) *http.Response { + return &http.Response{ + StatusCode: statusCode, + Header: http.Header{"Content-Type": []string{contentType}}, + Body: io.NopCloser(strings.NewReader(body)), + } +} + +func TestRetryingRoundTripper_RoundTrip(t *testing.T) { + marshalErr := func(code int, msg string) string { + b, _ := json.Marshal(kubernetesError{ + Code: code, + Message: msg, + }) + return string(b) + } + + tests := []struct { + name string + resp *http.Response + err error + expectedCalls int + expectedErr string + expectedCode int + }{ + { + name: "no retry, status < 500 returns response", + resp: newRespWithBody(200, "application/json", `{"message":"ok","code":200}`), + err: nil, + expectedCalls: 1, + expectedCode: 200, + }, + { + name: "error from wrapped RoundTripper propagates", + resp: nil, + err: errors.New("wrapped error"), + expectedCalls: 1, + expectedErr: "wrapped error", + }, + { + name: "no retry, content-type not application/json", + resp: newRespWithBody(500, "text/plain", "server error"), + err: nil, + expectedCalls: 1, + expectedCode: 500, + }, + { + name: "error reading body returns error", + resp: &http.Response{ + StatusCode: http.StatusInternalServerError, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: &errReader{}, + }, + err: nil, + expectedCalls: 1, + expectedErr: "read error", + }, + { + name: "error decoding JSON returns error", + resp: newRespWithBody(500, "application/json", `invalid-json`), + err: nil, + expectedCalls: 1, + expectedErr: "invalid character", + }, + { + name: "retry on etcdserver leader changed message", + resp: newRespWithBody(500, "application/json", marshalErr(500, "some error etcdserver: leader changed")), + err: nil, + expectedCalls: 2, + expectedCode: 500, + }, + { + name: "retry on raft proposal dropped message", + resp: newRespWithBody(500, "application/json", marshalErr(500, "rpc error: code = Unknown desc = raft proposal dropped")), + err: nil, + expectedCalls: 2, + expectedCode: 500, + }, + { + name: "no retry on other error message", + resp: newRespWithBody(500, "application/json", marshalErr(500, "other server error")), + err: nil, + expectedCalls: 1, + expectedCode: 500, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeRT := &fakeRoundTripper{ + resp: tt.resp, + err: tt.err, + } + rt := RetryingRoundTripper{ + Wrapped: fakeRT, + } + req, _ := http.NewRequest(http.MethodGet, "http://example.com", nil) + resp, err := rt.RoundTrip(req) + + if tt.expectedErr != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedErr) + return + } + assert.NoError(t, err) + + assert.Equal(t, tt.expectedCode, resp.StatusCode) + assert.Equal(t, tt.expectedCalls, fakeRT.calls) + }) + } +} + +type errReader struct{} + +func (e *errReader) Read(_ []byte) (int, error) { + return 0, errors.New("read error") +} + +func (e *errReader) Close() error { + return nil +} diff --git a/pkg/helm/pkg/kube/statuswait.go b/pkg/helm/pkg/kube/statuswait.go new file mode 100644 index 00000000..4484c351 --- /dev/null +++ b/pkg/helm/pkg/kube/statuswait.go @@ -0,0 +1,292 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" + +import ( + "context" + "errors" + "fmt" + "log/slog" + "sort" + "time" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/aggregator" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/collector" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/statusreaders" + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + "github.com/fluxcd/cli-utils/pkg/kstatus/watcher" + "github.com/fluxcd/cli-utils/pkg/object" + appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" + watchtools "k8s.io/client-go/tools/watch" + + "github.com/werf/nelm/pkg/helm/intern/logging" + helmStatusReaders "github.com/werf/nelm/pkg/helm/intern/statusreaders" +) + +type statusWaiter struct { + client dynamic.Interface + restMapper meta.RESTMapper + ctx context.Context + watchUntilReadyCtx context.Context + waitCtx context.Context + waitWithJobsCtx context.Context + waitForDeleteCtx context.Context + readers []engine.StatusReader + logging.LogHolder +} + +// DefaultStatusWatcherTimeout is the timeout used by the status waiter when a +// zero timeout is provided. This prevents callers from accidentally passing a +// zero value (which would immediately cancel the context) and getting +// "context deadline exceeded" errors. SDK callers can rely on this default +// when they don't set a timeout. +var DefaultStatusWatcherTimeout = 30 * time.Second + +func alwaysReady(_ *unstructured.Unstructured) (*status.Result, error) { + return &status.Result{ + Status: status.CurrentStatus, + Message: "Resource is current", + }, nil +} + +func (w *statusWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error { + if timeout == 0 { + timeout = DefaultStatusWatcherTimeout + } + ctx, cancel := w.contextWithTimeout(w.watchUntilReadyCtx, timeout) + defer cancel() + w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout) + sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + jobSR := helmStatusReaders.NewCustomJobStatusReader(w.restMapper) + podSR := helmStatusReaders.NewCustomPodStatusReader(w.restMapper) + // We don't want to wait on any other resources as watchUntilReady is only for Helm hooks. + // If custom readers are defined they can be used as Helm hooks support any resource. + // We put them in front since the DelegatingStatusReader uses the first reader that matches. + genericSR := statusreaders.NewGenericStatusReader(w.restMapper, alwaysReady) + + sr := &statusreaders.DelegatingStatusReader{ + StatusReaders: append(w.readers, jobSR, podSR, genericSR), + } + sw.StatusReader = sr + return w.wait(ctx, resourceList, sw) +} + +func (w *statusWaiter) Wait(resourceList ResourceList, timeout time.Duration) error { + if timeout == 0 { + timeout = DefaultStatusWatcherTimeout + } + ctx, cancel := w.contextWithTimeout(w.waitCtx, timeout) + defer cancel() + w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout) + sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + sw.StatusReader = statusreaders.NewStatusReader(w.restMapper, w.readers...) + return w.wait(ctx, resourceList, sw) +} + +func (w *statusWaiter) WaitWithJobs(resourceList ResourceList, timeout time.Duration) error { + if timeout == 0 { + timeout = DefaultStatusWatcherTimeout + } + ctx, cancel := w.contextWithTimeout(w.waitWithJobsCtx, timeout) + defer cancel() + w.Logger().Debug("waiting for resources", "count", len(resourceList), "timeout", timeout) + sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + newCustomJobStatusReader := helmStatusReaders.NewCustomJobStatusReader(w.restMapper) + readers := append([]engine.StatusReader(nil), w.readers...) + readers = append(readers, newCustomJobStatusReader) + customSR := statusreaders.NewStatusReader(w.restMapper, readers...) + sw.StatusReader = customSR + return w.wait(ctx, resourceList, sw) +} + +func (w *statusWaiter) WaitForDelete(resourceList ResourceList, timeout time.Duration) error { + if timeout == 0 { + timeout = DefaultStatusWatcherTimeout + } + ctx, cancel := w.contextWithTimeout(w.waitForDeleteCtx, timeout) + defer cancel() + w.Logger().Debug("waiting for resources to be deleted", "count", len(resourceList), "timeout", timeout) + sw := watcher.NewDefaultStatusWatcher(w.client, w.restMapper) + return w.waitForDelete(ctx, resourceList, sw) +} + +func (w *statusWaiter) waitForDelete(ctx context.Context, resourceList ResourceList, sw watcher.StatusWatcher) error { + cancelCtx, cancel := context.WithCancel(ctx) + defer cancel() + resources := []object.ObjMetadata{} + for _, resource := range resourceList { + obj, err := object.RuntimeToObjMeta(resource.Object) + if err != nil { + return err + } + resources = append(resources, obj) + } + eventCh := sw.Watch(cancelCtx, resources, watcher.Options{ + RESTScopeStrategy: watcher.RESTScopeNamespace, + }) + statusCollector := collector.NewResourceStatusCollector(resources) + done := statusCollector.ListenWithObserver(eventCh, statusObserver(cancel, status.NotFoundStatus, w.Logger())) + <-done + + if statusCollector.Error != nil { + return statusCollector.Error + } + + errs := []error{} + for _, id := range resources { + rs := statusCollector.ResourceStatuses[id] + if rs.Status == status.NotFoundStatus || rs.Status == status.UnknownStatus { + continue + } + errs = append(errs, fmt.Errorf("resource %s/%s/%s still exists. status: %s, message: %s", + rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message)) + } + if err := ctx.Err(); err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +func (w *statusWaiter) wait(ctx context.Context, resourceList ResourceList, sw watcher.StatusWatcher) error { + cancelCtx, cancel := context.WithCancel(ctx) + defer cancel() + resources := []object.ObjMetadata{} + for _, resource := range resourceList { + switch value := AsVersioned(resource).(type) { + case *appsv1.Deployment: + if value.Spec.Paused { + continue + } + } + obj, err := object.RuntimeToObjMeta(resource.Object) + if err != nil { + return err + } + resources = append(resources, obj) + } + + eventCh := sw.Watch(cancelCtx, resources, watcher.Options{ + RESTScopeStrategy: watcher.RESTScopeNamespace, + }) + statusCollector := collector.NewResourceStatusCollector(resources) + done := statusCollector.ListenWithObserver(eventCh, statusObserver(cancel, status.CurrentStatus, w.Logger())) + <-done + + if statusCollector.Error != nil { + return statusCollector.Error + } + + errs := []error{} + for _, id := range resources { + rs := statusCollector.ResourceStatuses[id] + if rs.Status == status.CurrentStatus { + continue + } + errs = append(errs, fmt.Errorf("resource %s/%s/%s not ready. status: %s, message: %s", + rs.Identifier.GroupKind.Kind, rs.Identifier.Namespace, rs.Identifier.Name, rs.Status, rs.Message)) + } + if err := ctx.Err(); err != nil { + errs = append(errs, err) + } + if len(errs) > 0 { + return errors.Join(errs...) + } + return nil +} + +func (w *statusWaiter) contextWithTimeout(methodCtx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { + if methodCtx == nil { + methodCtx = w.ctx + } + return contextWithTimeout(methodCtx, timeout) +} + +func contextWithTimeout(ctx context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { + if ctx == nil { + ctx = context.Background() + } + return watchtools.ContextWithOptionalTimeout(ctx, timeout) +} + +func statusObserver(cancel context.CancelFunc, desired status.Status, logger *slog.Logger) collector.ObserverFunc { + return func(statusCollector *collector.ResourceStatusCollector, _ event.Event) { + var rss []*event.ResourceStatus + var nonDesiredResources []*event.ResourceStatus + for _, rs := range statusCollector.ResourceStatuses { + if rs == nil { + continue + } + // If a resource is already deleted before waiting has started, it will show as unknown. + // This check ensures we don't wait forever for a resource that is already deleted. + if rs.Status == status.UnknownStatus && desired == status.NotFoundStatus { + continue + } + // Failed is a terminal state. This check ensures we don't wait forever for a resource + // that has already failed, as intervention is required to resolve the failure. + if rs.Status == status.FailedStatus && desired == status.CurrentStatus { + continue + } + rss = append(rss, rs) + if rs.Status != desired { + nonDesiredResources = append(nonDesiredResources, rs) + } + } + + if aggregator.AggregateStatus(rss, desired) == desired { + logger.Debug("all resources achieved desired status", "desiredStatus", desired, "resourceCount", len(rss)) + cancel() + return + } + + if len(nonDesiredResources) > 0 { + // Log a single resource so the user knows what they're waiting for without an overwhelming amount of output + sort.Slice(nonDesiredResources, func(i, j int) bool { + return nonDesiredResources[i].Identifier.Name < nonDesiredResources[j].Identifier.Name + }) + first := nonDesiredResources[0] + logger.Debug("waiting for resource", "namespace", first.Identifier.Namespace, "name", first.Identifier.Name, "kind", first.Identifier.GroupKind.Kind, "expectedStatus", desired, "actualStatus", first.Status) + } + } +} + +type hookOnlyWaiter struct { + sw *statusWaiter +} + +func (w *hookOnlyWaiter) WatchUntilReady(resourceList ResourceList, timeout time.Duration) error { + return w.sw.WatchUntilReady(resourceList, timeout) +} + +func (w *hookOnlyWaiter) Wait(_ ResourceList, _ time.Duration) error { + return nil +} + +func (w *hookOnlyWaiter) WaitWithJobs(_ ResourceList, _ time.Duration) error { + return nil +} + +func (w *hookOnlyWaiter) WaitForDelete(_ ResourceList, _ time.Duration) error { + return nil +} diff --git a/pkg/helm/pkg/kube/statuswait_test.go b/pkg/helm/pkg/kube/statuswait_test.go new file mode 100644 index 00000000..85aaf879 --- /dev/null +++ b/pkg/helm/pkg/kube/statuswait_test.go @@ -0,0 +1,1820 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/engine" + "github.com/fluxcd/cli-utils/pkg/kstatus/polling/event" + "github.com/fluxcd/cli-utils/pkg/kstatus/status" + "github.com/fluxcd/cli-utils/pkg/object" + "github.com/fluxcd/cli-utils/pkg/testutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/apimachinery/pkg/watch" + dynamicfake "k8s.io/client-go/dynamic/fake" + clienttesting "k8s.io/client-go/testing" + "k8s.io/kubectl/pkg/scheme" +) + +var podCurrentManifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: current-pod + namespace: ns +status: + conditions: + - type: Ready + status: "True" + phase: Running +` + +var podNoStatusManifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: in-progress-pod + namespace: ns +` + +var jobNoStatusManifest = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual + generation: 1 +` + +var jobReadyManifest = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: ready-not-complete + namespace: default + generation: 1 +status: + startTime: 2025-02-06T16:34:20-05:00 + active: 1 + ready: 1 +` + +var jobCompleteManifest = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: test + namespace: qual + generation: 1 +status: + succeeded: 1 + active: 0 + conditions: + - type: Complete + status: "True" +` + +var jobFailedManifest = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: failed-job + namespace: default + generation: 1 +status: + failed: 1 + active: 0 + conditions: + - type: Failed + status: "True" + reason: BackoffLimitExceeded + message: "Job has reached the specified backoff limit" +` + +var podCompleteManifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: good-pod + namespace: ns +status: + phase: Succeeded +` + +var pausedDeploymentManifest = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: paused + namespace: ns-1 + generation: 1 +spec: + paused: true + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.19.6 + ports: + - containerPort: 80 +` + +var notReadyDeploymentManifest = ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: not-ready + namespace: ns-1 + generation: 1 +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.19.6 + ports: + - containerPort: 80 +` + +var podNamespace1Manifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: pod-ns1 + namespace: namespace-1 +status: + conditions: + - type: Ready + status: "True" + phase: Running +` + +var podNamespace2Manifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: pod-ns2 + namespace: namespace-2 +status: + conditions: + - type: Ready + status: "True" + phase: Running +` + +var podNamespace1NoStatusManifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: pod-ns1 + namespace: namespace-1 +` + +var jobNamespace1CompleteManifest = ` +apiVersion: batch/v1 +kind: Job +metadata: + name: job-ns1 + namespace: namespace-1 + generation: 1 +status: + succeeded: 1 + active: 0 + conditions: + - type: Complete + status: "True" +` + +var podNamespace2SucceededManifest = ` +apiVersion: v1 +kind: Pod +metadata: + name: pod-ns2 + namespace: namespace-2 +status: + phase: Succeeded +` + +var clusterRoleManifest = ` +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: test-cluster-role +rules: +- apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] +` + +var namespaceManifest = ` +apiVersion: v1 +kind: Namespace +metadata: + name: test-namespace +` + +func getGVR(t *testing.T, mapper meta.RESTMapper, obj *unstructured.Unstructured) schema.GroupVersionResource { + t.Helper() + gvk := obj.GroupVersionKind() + mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) + require.NoError(t, err) + return mapping.Resource +} + +func getRuntimeObjFromManifests(t *testing.T, manifests []string) []runtime.Object { + t.Helper() + objects := []runtime.Object{} + for _, manifest := range manifests { + m := make(map[string]interface{}) + err := yaml.Unmarshal([]byte(manifest), &m) + assert.NoError(t, err) + resource := &unstructured.Unstructured{Object: m} + objects = append(objects, resource) + } + return objects +} + +func getResourceListFromRuntimeObjs(t *testing.T, c *Client, objs []runtime.Object) ResourceList { + t.Helper() + resourceList := ResourceList{} + for _, obj := range objs { + list, err := c.Build(objBody(obj), false) + assert.NoError(t, err) + resourceList = append(resourceList, list...) + } + return resourceList +} + +func TestStatusWaitForDelete(t *testing.T) { + t.Parallel() + tests := []struct { + name string + manifestsToCreate []string + manifestsToDelete []string + expectErrs []string + }{ + { + name: "wait for pod to be deleted", + manifestsToCreate: []string{podCurrentManifest}, + manifestsToDelete: []string{podCurrentManifest}, + expectErrs: nil, + }, + { + name: "error when not all objects are deleted", + manifestsToCreate: []string{jobCompleteManifest, podCurrentManifest}, + manifestsToDelete: []string{jobCompleteManifest}, + expectErrs: []string{"resource Pod/ns/current-pod still exists. status: Current", "context deadline exceeded"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + timeout := time.Second + timeUntilPodDelete := time.Millisecond * 500 + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + restMapper: fakeMapper, + client: fakeClient, + } + statusWaiter.SetLogger(slog.Default().Handler()) + objsToCreate := getRuntimeObjFromManifests(t, tt.manifestsToCreate) + for _, objToCreate := range objsToCreate { + u := objToCreate.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + objsToDelete := getRuntimeObjFromManifests(t, tt.manifestsToDelete) + for _, objToDelete := range objsToDelete { + u := objToDelete.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + go func(gvr schema.GroupVersionResource, u *unstructured.Unstructured) { + time.Sleep(timeUntilPodDelete) + err := fakeClient.Tracker().Delete(gvr, u.GetNamespace(), u.GetName()) + assert.NoError(t, err) + }(gvr, u) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objsToCreate) + err := statusWaiter.WaitForDelete(resourceList, timeout) + if tt.expectErrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +func TestStatusWaitForDeleteNonExistentObject(t *testing.T) { + t.Parallel() + c := newTestClient(t) + timeout := time.Second + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + statusWaiter := statusWaiter{ + restMapper: fakeMapper, + client: fakeClient, + } + statusWaiter.SetLogger(slog.Default().Handler()) + // Don't create the object to test that the wait for delete works when the object doesn't exist + objManifest := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + resourceList := getResourceListFromRuntimeObjs(t, c, objManifest) + err := statusWaiter.WaitForDelete(resourceList, timeout) + assert.NoError(t, err) +} + +func TestStatusWait(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + expectErrStrs []string + waitForJobs bool + }{ + { + name: "Job is not complete", + objManifests: []string{jobNoStatusManifest}, + expectErrStrs: []string{"resource Job/qual/test not ready. status: InProgress", "context deadline exceeded"}, + waitForJobs: true, + }, + { + name: "Job is ready but not complete", + objManifests: []string{jobReadyManifest}, + expectErrStrs: nil, + waitForJobs: false, + }, + { + name: "Pod is ready", + objManifests: []string{podCurrentManifest}, + }, + { + name: "one of the pods never becomes ready", + objManifests: []string{podNoStatusManifest, podCurrentManifest}, + expectErrStrs: []string{"resource Pod/ns/in-progress-pod not ready. status: InProgress", "context deadline exceeded"}, + }, + { + name: "paused deployment passes", + objManifests: []string{pausedDeploymentManifest}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + appsv1.SchemeGroupVersion.WithKind("Deployment"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + } + statusWaiter.SetLogger(slog.Default().Handler()) + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := statusWaiter.Wait(resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +func TestWaitForJobComplete(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + expectErrStrs []string + }{ + { + name: "Job is complete", + objManifests: []string{jobCompleteManifest}, + }, + { + name: "Job is not ready", + objManifests: []string{jobNoStatusManifest}, + expectErrStrs: []string{"resource Job/qual/test not ready. status: InProgress", "context deadline exceeded"}, + }, + { + name: "Job is ready but not complete", + objManifests: []string{jobReadyManifest}, + expectErrStrs: []string{"resource Job/default/ready-not-complete not ready. status: InProgress", "context deadline exceeded"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + } + statusWaiter.SetLogger(slog.Default().Handler()) + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := statusWaiter.WaitWithJobs(resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +func TestWatchForReady(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + expectErrStrs []string + }{ + { + name: "succeeds if pod and job are complete", + objManifests: []string{jobCompleteManifest, podCompleteManifest}, + }, + { + name: "succeeds when a resource that's not a pod or job is not ready", + objManifests: []string{notReadyDeploymentManifest}, + }, + { + name: "Fails if job is not complete", + objManifests: []string{jobReadyManifest}, + expectErrStrs: []string{"resource Job/default/ready-not-complete not ready. status: InProgress", "context deadline exceeded"}, + }, + { + name: "Fails if pod is not complete", + objManifests: []string{podCurrentManifest}, + expectErrStrs: []string{"resource Pod/ns/current-pod not ready. status: InProgress", "context deadline exceeded"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + appsv1.SchemeGroupVersion.WithKind("Deployment"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + } + statusWaiter.SetLogger(slog.Default().Handler()) + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := statusWaiter.WatchUntilReady(resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +func TestStatusWaitMultipleNamespaces(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + expectErrStrs []string + testFunc func(*statusWaiter, ResourceList, time.Duration) error + }{ + { + name: "pods in multiple namespaces", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "hooks in multiple namespaces", + objManifests: []string{jobNamespace1CompleteManifest, podNamespace2SucceededManifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WatchUntilReady(rl, timeout) + }, + }, + { + name: "error when resource not ready in one namespace", + objManifests: []string{podNamespace1NoStatusManifest, podNamespace2Manifest}, + expectErrStrs: []string{"resource Pod/namespace-1/pod-ns1 not ready. status: InProgress", "context deadline exceeded"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "delete resources in multiple namespaces", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WaitForDelete(rl, timeout) + }, + }, + { + name: "cluster-scoped resources work correctly with unrestricted permissions", + objManifests: []string{podNamespace1Manifest, clusterRoleManifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "namespace-scoped and cluster-scoped resources work together", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest, clusterRoleManifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "delete cluster-scoped resources works correctly", + objManifests: []string{podNamespace1Manifest, namespaceManifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WaitForDelete(rl, timeout) + }, + }, + { + name: "watch cluster-scoped resources works correctly", + objManifests: []string{clusterRoleManifest}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WatchUntilReady(rl, timeout) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1"}.WithKind("ClusterRole"), + v1.SchemeGroupVersion.WithKind("Namespace"), + ) + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + } + sw.SetLogger(slog.Default().Handler()) + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + + if strings.Contains(tt.name, "delete") { + timeUntilDelete := time.Millisecond * 500 + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + go func(gvr schema.GroupVersionResource, u *unstructured.Unstructured) { + time.Sleep(timeUntilDelete) + err := fakeClient.Tracker().Delete(gvr, u.GetNamespace(), u.GetName()) + assert.NoError(t, err) + }(gvr, u) + } + } + + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := tt.testFunc(&sw, resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +// restrictedClientConfig holds the configuration for RBAC simulation on a fake dynamic client +type restrictedClientConfig struct { + allowedNamespaces map[string]bool + clusterScopedListAttempted bool +} + +// setupRestrictedClient configures a fake dynamic client to simulate RBAC restrictions +// by using PrependReactor and PrependWatchReactor to intercept list/watch operations. +func setupRestrictedClient(fakeClient *dynamicfake.FakeDynamicClient, allowedNamespaces []string) *restrictedClientConfig { + allowed := make(map[string]bool) + for _, ns := range allowedNamespaces { + allowed[ns] = true + } + config := &restrictedClientConfig{ + allowedNamespaces: allowed, + } + + // Intercept list operations + fakeClient.PrependReactor("list", "*", func(action clienttesting.Action) (bool, runtime.Object, error) { + listAction := action.(clienttesting.ListAction) + ns := listAction.GetNamespace() + if ns == "" { + // Cluster-scoped list + config.clusterScopedListAttempted = true + return true, nil, apierrors.NewForbidden( + action.GetResource().GroupResource(), + "", + fmt.Errorf("user does not have cluster-wide LIST permissions for cluster-scoped resources"), + ) + } + if !config.allowedNamespaces[ns] { + return true, nil, apierrors.NewForbidden( + action.GetResource().GroupResource(), + "", + fmt.Errorf("user does not have LIST permissions in namespace %q", ns), + ) + } + // Fall through to the default handler + return false, nil, nil + }) + + // Intercept watch operations + fakeClient.PrependWatchReactor("*", func(action clienttesting.Action) (bool, watch.Interface, error) { + watchAction := action.(clienttesting.WatchAction) + ns := watchAction.GetNamespace() + if ns == "" { + // Cluster-scoped watch + config.clusterScopedListAttempted = true + return true, nil, apierrors.NewForbidden( + action.GetResource().GroupResource(), + "", + fmt.Errorf("user does not have cluster-wide WATCH permissions for cluster-scoped resources"), + ) + } + if !config.allowedNamespaces[ns] { + return true, nil, apierrors.NewForbidden( + action.GetResource().GroupResource(), + "", + fmt.Errorf("user does not have WATCH permissions in namespace %q", ns), + ) + } + // Fall through to the default handler + return false, nil, nil + }) + + return config +} + +func TestStatusWaitRestrictedRBAC(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + allowedNamespaces []string + expectErrs []error + testFunc func(*statusWaiter, ResourceList, time.Duration) error + }{ + { + name: "pods in multiple namespaces with namespace permissions", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + allowedNamespaces: []string{"namespace-1", "namespace-2"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "delete pods in multiple namespaces with namespace permissions", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + allowedNamespaces: []string{"namespace-1", "namespace-2"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WaitForDelete(rl, timeout) + }, + }, + { + name: "hooks in multiple namespaces with namespace permissions", + objManifests: []string{jobNamespace1CompleteManifest, podNamespace2SucceededManifest}, + allowedNamespaces: []string{"namespace-1", "namespace-2"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WatchUntilReady(rl, timeout) + }, + }, + { + name: "error when cluster-scoped resource included", + objManifests: []string{podNamespace1Manifest, clusterRoleManifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have cluster-wide LIST permissions for cluster-scoped resources")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "error when deleting cluster-scoped resource", + objManifests: []string{podNamespace1Manifest, namespaceManifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have cluster-wide LIST permissions for cluster-scoped resources")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WaitForDelete(rl, timeout) + }, + }, + { + name: "error when accessing disallowed namespace", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have LIST permissions in namespace %q", "namespace-2")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + baseFakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1"}.WithKind("ClusterRole"), + v1.SchemeGroupVersion.WithKind("Namespace"), + ) + restrictedConfig := setupRestrictedClient(baseFakeClient, tt.allowedNamespaces) + sw := statusWaiter{ + client: baseFakeClient, + restMapper: fakeMapper, + } + sw.SetLogger(slog.Default().Handler()) + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := baseFakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + + if strings.Contains(tt.name, "delet") { + timeUntilDelete := time.Millisecond * 500 + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + go func(gvr schema.GroupVersionResource, u *unstructured.Unstructured) { + time.Sleep(timeUntilDelete) + err := baseFakeClient.Tracker().Delete(gvr, u.GetNamespace(), u.GetName()) + assert.NoError(t, err) + }(gvr, u) + } + } + + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := tt.testFunc(&sw, resourceList, time.Second*3) + if tt.expectErrs != nil { + require.Error(t, err) + for _, expectedErr := range tt.expectErrs { + assert.Contains(t, err.Error(), expectedErr.Error()) + } + return + } + assert.NoError(t, err) + assert.False(t, restrictedConfig.clusterScopedListAttempted) + }) + } +} + +func TestStatusWaitMixedResources(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + allowedNamespaces []string + expectErrs []error + testFunc func(*statusWaiter, ResourceList, time.Duration) error + }{ + { + name: "wait succeeds with namespace-scoped resources only", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + allowedNamespaces: []string{"namespace-1", "namespace-2"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "wait fails when cluster-scoped resource included", + objManifests: []string{podNamespace1Manifest, clusterRoleManifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have cluster-wide LIST permissions for cluster-scoped resources")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "waitForDelete fails when cluster-scoped resource included", + objManifests: []string{podNamespace1Manifest, clusterRoleManifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have cluster-wide LIST permissions for cluster-scoped resources")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WaitForDelete(rl, timeout) + }, + }, + { + name: "wait fails when namespace resource included", + objManifests: []string{podNamespace1Manifest, namespaceManifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have cluster-wide LIST permissions for cluster-scoped resources")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "error when accessing disallowed namespace", + objManifests: []string{podNamespace1Manifest, podNamespace2Manifest}, + allowedNamespaces: []string{"namespace-1"}, + expectErrs: []error{fmt.Errorf("user does not have LIST permissions in namespace %q", "namespace-2")}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + baseFakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + schema.GroupVersion{Group: "rbac.authorization.k8s.io", Version: "v1"}.WithKind("ClusterRole"), + v1.SchemeGroupVersion.WithKind("Namespace"), + ) + restrictedConfig := setupRestrictedClient(baseFakeClient, tt.allowedNamespaces) + sw := statusWaiter{ + client: baseFakeClient, + restMapper: fakeMapper, + } + sw.SetLogger(slog.Default().Handler()) + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := baseFakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + + if strings.Contains(tt.name, "delet") { + timeUntilDelete := time.Millisecond * 500 + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + go func(gvr schema.GroupVersionResource, u *unstructured.Unstructured) { + time.Sleep(timeUntilDelete) + err := baseFakeClient.Tracker().Delete(gvr, u.GetNamespace(), u.GetName()) + assert.NoError(t, err) + }(gvr, u) + } + } + + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := tt.testFunc(&sw, resourceList, time.Second*3) + if tt.expectErrs != nil { + require.Error(t, err) + for _, expectedErr := range tt.expectErrs { + assert.Contains(t, err.Error(), expectedErr.Error()) + } + return + } + assert.NoError(t, err) + assert.False(t, restrictedConfig.clusterScopedListAttempted) + }) + } +} + +// mockStatusReader is a custom status reader for testing that tracks when it's used +// and returns a configurable status for resources it supports. +type mockStatusReader struct { + supportedGK schema.GroupKind + status status.Status + callCount atomic.Int32 +} + +func (m *mockStatusReader) Supports(gk schema.GroupKind) bool { + return gk == m.supportedGK +} + +func (m *mockStatusReader) ReadStatus(_ context.Context, _ engine.ClusterReader, id object.ObjMetadata) (*event.ResourceStatus, error) { + m.callCount.Add(1) + return &event.ResourceStatus{ + Identifier: id, + Status: m.status, + Message: "mock status reader", + }, nil +} + +func (m *mockStatusReader) ReadStatusForObject(_ context.Context, _ engine.ClusterReader, u *unstructured.Unstructured) (*event.ResourceStatus, error) { + m.callCount.Add(1) + id := object.ObjMetadata{ + Namespace: u.GetNamespace(), + Name: u.GetName(), + GroupKind: u.GroupVersionKind().GroupKind(), + } + return &event.ResourceStatus{ + Identifier: id, + Status: m.status, + Message: "mock status reader", + }, nil +} + +func TestStatusWaitWithCustomReaders(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + customReader *mockStatusReader + expectErrStrs []string + }{ + { + name: "custom reader makes pod immediately current", + objManifests: []string{podNoStatusManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.CurrentStatus, + }, + }, + { + name: "custom reader returns in-progress status", + objManifests: []string{podCurrentManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.InProgressStatus, + }, + expectErrStrs: []string{"resource Pod/ns/current-pod not ready. status: InProgress", "context deadline exceeded"}, + }, + { + name: "custom reader for different resource type is not used", + objManifests: []string{podCurrentManifest}, + customReader: &mockStatusReader{ + supportedGK: batchv1.SchemeGroupVersion.WithKind("Job").GroupKind(), + status: status.InProgressStatus, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + readers: []engine.StatusReader{tt.customReader}, + } + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := statusWaiter.Wait(resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +func TestStatusWaitWithJobsAndCustomReaders(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + customReader *mockStatusReader + expectErrs []error + }{ + { + name: "custom reader makes job immediately current", + objManifests: []string{jobNoStatusManifest}, + customReader: &mockStatusReader{ + supportedGK: batchv1.SchemeGroupVersion.WithKind("Job").GroupKind(), + status: status.CurrentStatus, + }, + expectErrs: nil, + }, + { + name: "custom reader for pod works with WaitWithJobs", + objManifests: []string{podNoStatusManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.CurrentStatus, + }, + expectErrs: nil, + }, + { + name: "built-in job reader is still appended after custom readers", + objManifests: []string{jobCompleteManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.CurrentStatus, + }, + expectErrs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + readers: []engine.StatusReader{tt.customReader}, + } + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := statusWaiter.WaitWithJobs(resourceList, time.Second*3) + if tt.expectErrs != nil { + assert.EqualError(t, err, errors.Join(tt.expectErrs...).Error()) + return + } + assert.NoError(t, err) + }) + } +} + +func TestStatusWaitWithFailedResources(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + customReader *mockStatusReader + expectErrStrs []string + testFunc func(*statusWaiter, ResourceList, time.Duration) error + }{ + { + name: "Wait returns error when resource has failed", + objManifests: []string{podNoStatusManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.FailedStatus, + }, + expectErrStrs: []string{"resource Pod/ns/in-progress-pod not ready. status: Failed, message: mock status reader"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "WaitWithJobs returns error when job has failed", + objManifests: []string{jobFailedManifest}, + customReader: nil, // Use the built-in job status reader + expectErrStrs: []string{ + "resource Job/default/failed-job not ready. status: Failed", + }, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WaitWithJobs(rl, timeout) + }, + }, + { + name: "Wait returns errors when multiple resources fail", + objManifests: []string{podNoStatusManifest, podCurrentManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.FailedStatus, + }, + // The mock reader will make both pods return FailedStatus + expectErrStrs: []string{ + "resource Pod/ns/in-progress-pod not ready. status: Failed, message: mock status reader", + "resource Pod/ns/current-pod not ready. status: Failed, message: mock status reader", + }, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.Wait(rl, timeout) + }, + }, + { + name: "WatchUntilReady returns error when resource has failed", + objManifests: []string{podNoStatusManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.FailedStatus, + }, + // WatchUntilReady also waits for CurrentStatus, so failed resources should return error + expectErrStrs: []string{"resource Pod/ns/in-progress-pod not ready. status: Failed, message: mock status reader"}, + testFunc: func(sw *statusWaiter, rl ResourceList, timeout time.Duration) error { + return sw.WatchUntilReady(rl, timeout) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + var readers []engine.StatusReader + if tt.customReader != nil { + readers = []engine.StatusReader{tt.customReader} + } + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + readers: readers, + } + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := tt.testFunc(&sw, resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} + +func TestWaitOptionFunctions(t *testing.T) { + t.Parallel() + + t.Run("WithWatchUntilReadyMethodContext sets watchUntilReadyCtx", func(t *testing.T) { + t.Parallel() + type contextKey struct{} + ctx := context.WithValue(context.Background(), contextKey{}, "test") + opts := &waitOptions{} + WithWatchUntilReadyMethodContext(ctx)(opts) + assert.Equal(t, ctx, opts.watchUntilReadyCtx) + }) + + t.Run("WithWaitMethodContext sets waitCtx", func(t *testing.T) { + t.Parallel() + type contextKey struct{} + ctx := context.WithValue(context.Background(), contextKey{}, "test") + opts := &waitOptions{} + WithWaitMethodContext(ctx)(opts) + assert.Equal(t, ctx, opts.waitCtx) + }) + + t.Run("WithWaitWithJobsMethodContext sets waitWithJobsCtx", func(t *testing.T) { + t.Parallel() + type contextKey struct{} + ctx := context.WithValue(context.Background(), contextKey{}, "test") + opts := &waitOptions{} + WithWaitWithJobsMethodContext(ctx)(opts) + assert.Equal(t, ctx, opts.waitWithJobsCtx) + }) + + t.Run("WithWaitForDeleteMethodContext sets waitForDeleteCtx", func(t *testing.T) { + t.Parallel() + type contextKey struct{} + ctx := context.WithValue(context.Background(), contextKey{}, "test") + opts := &waitOptions{} + WithWaitForDeleteMethodContext(ctx)(opts) + assert.Equal(t, ctx, opts.waitForDeleteCtx) + }) +} + +func TestMethodSpecificContextCancellation(t *testing.T) { + t.Parallel() + + t.Run("WatchUntilReady uses method-specific context", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // Create a cancelled method-specific context + methodCtx, methodCancel := context.WithCancel(context.Background()) + methodCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: context.Background(), // General context is not cancelled + watchUntilReadyCtx: methodCtx, // Method context is cancelled + } + + objs := getRuntimeObjFromManifests(t, []string{podCompleteManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WatchUntilReady(resourceList, time.Second*3) + // Should fail due to cancelled method context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("Wait uses method-specific context", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // Create a cancelled method-specific context + methodCtx, methodCancel := context.WithCancel(context.Background()) + methodCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: context.Background(), // General context is not cancelled + waitCtx: methodCtx, // Method context is cancelled + } + + objs := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.Wait(resourceList, time.Second*3) + // Should fail due to cancelled method context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("WaitWithJobs uses method-specific context", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + + // Create a cancelled method-specific context + methodCtx, methodCancel := context.WithCancel(context.Background()) + methodCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: context.Background(), // General context is not cancelled + waitWithJobsCtx: methodCtx, // Method context is cancelled + } + + objs := getRuntimeObjFromManifests(t, []string{jobCompleteManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WaitWithJobs(resourceList, time.Second*3) + // Should fail due to cancelled method context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("WaitForDelete uses method-specific context", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // Create a cancelled method-specific context + methodCtx, methodCancel := context.WithCancel(context.Background()) + methodCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: context.Background(), // General context is not cancelled + waitForDeleteCtx: methodCtx, // Method context is cancelled + } + + objs := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WaitForDelete(resourceList, time.Second*3) + // Should fail due to cancelled method context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) +} + +func TestMethodContextFallbackToGeneralContext(t *testing.T) { + t.Parallel() + + t.Run("WatchUntilReady falls back to general context when method context is nil", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // Create a cancelled general context + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // General context is cancelled + watchUntilReadyCtx: nil, // Method context is nil, should fall back + } + + objs := getRuntimeObjFromManifests(t, []string{podCompleteManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WatchUntilReady(resourceList, time.Second*3) + // Should fail due to cancelled general context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("Wait falls back to general context when method context is nil", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // Create a cancelled general context + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // General context is cancelled + waitCtx: nil, // Method context is nil, should fall back + } + + objs := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.Wait(resourceList, time.Second*3) + // Should fail due to cancelled general context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("WaitWithJobs falls back to general context when method context is nil", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + + // Create a cancelled general context + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // General context is cancelled + waitWithJobsCtx: nil, // Method context is nil, should fall back + } + + objs := getRuntimeObjFromManifests(t, []string{jobCompleteManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WaitWithJobs(resourceList, time.Second*3) + // Should fail due to cancelled general context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) + + t.Run("WaitForDelete falls back to general context when method context is nil", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // Create a cancelled general context + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() // Cancel immediately + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // General context is cancelled + waitForDeleteCtx: nil, // Method context is nil, should fall back + } + + objs := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WaitForDelete(resourceList, time.Second*3) + // Should fail due to cancelled general context + require.Error(t, err) + assert.Contains(t, err.Error(), "context canceled") + }) +} + +func TestMethodContextOverridesGeneralContext(t *testing.T) { + t.Parallel() + + t.Run("method-specific context overrides general context for WatchUntilReady", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // General context is cancelled, but method context is not + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // Cancelled + watchUntilReadyCtx: context.Background(), // Not cancelled - should be used + } + + objs := getRuntimeObjFromManifests(t, []string{podCompleteManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WatchUntilReady(resourceList, time.Second*3) + // Should succeed because method context is used and it's not cancelled + assert.NoError(t, err) + }) + + t.Run("method-specific context overrides general context for Wait", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // General context is cancelled, but method context is not + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // Cancelled + waitCtx: context.Background(), // Not cancelled - should be used + } + + objs := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.Wait(resourceList, time.Second*3) + // Should succeed because method context is used and it's not cancelled + assert.NoError(t, err) + }) + + t.Run("method-specific context overrides general context for WaitWithJobs", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + + // General context is cancelled, but method context is not + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // Cancelled + waitWithJobsCtx: context.Background(), // Not cancelled - should be used + } + + objs := getRuntimeObjFromManifests(t, []string{jobCompleteManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + + err := sw.WaitWithJobs(resourceList, time.Second*3) + // Should succeed because method context is used and it's not cancelled + assert.NoError(t, err) + }) + + t.Run("method-specific context overrides general context for WaitForDelete", func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + timeout := time.Second + timeUntilPodDelete := time.Millisecond * 500 + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + ) + + // General context is cancelled, but method context is not + generalCtx, generalCancel := context.WithCancel(context.Background()) + generalCancel() + + sw := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + ctx: generalCtx, // Cancelled + waitForDeleteCtx: context.Background(), // Not cancelled - should be used + } + + objs := getRuntimeObjFromManifests(t, []string{podCurrentManifest}) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + require.NoError(t, err) + } + + // Schedule deletion + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + go func(gvr schema.GroupVersionResource, u *unstructured.Unstructured) { + time.Sleep(timeUntilPodDelete) + err := fakeClient.Tracker().Delete(gvr, u.GetNamespace(), u.GetName()) + assert.NoError(t, err) + }(gvr, u) + } + + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := sw.WaitForDelete(resourceList, timeout) + // Should succeed because method context is used and it's not cancelled + assert.NoError(t, err) + }) +} + +func TestWatchUntilReadyWithCustomReaders(t *testing.T) { + t.Parallel() + tests := []struct { + name string + objManifests []string + customReader *mockStatusReader + expectErrStrs []string + }{ + { + name: "custom reader makes job immediately current for hooks", + objManifests: []string{jobNoStatusManifest}, + customReader: &mockStatusReader{ + supportedGK: batchv1.SchemeGroupVersion.WithKind("Job").GroupKind(), + status: status.CurrentStatus, + }, + }, + { + name: "custom reader makes pod immediately current for hooks", + objManifests: []string{podCurrentManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.CurrentStatus, + }, + }, + { + name: "custom reader takes precedence over built-in pod reader", + objManifests: []string{podCompleteManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.InProgressStatus, + }, + expectErrStrs: []string{"resource Pod/ns/good-pod not ready. status: InProgress", "context deadline exceeded"}, + }, + { + name: "custom reader takes precedence over built-in job reader", + objManifests: []string{jobCompleteManifest}, + customReader: &mockStatusReader{ + supportedGK: batchv1.SchemeGroupVersion.WithKind("Job").GroupKind(), + status: status.InProgressStatus, + }, + expectErrStrs: []string{"resource Job/qual/test not ready. status: InProgress", "context deadline exceeded"}, + }, + { + name: "custom reader for different resource type does not affect pods", + objManifests: []string{podCompleteManifest}, + customReader: &mockStatusReader{ + supportedGK: batchv1.SchemeGroupVersion.WithKind("Job").GroupKind(), + status: status.InProgressStatus, + }, + }, + { + name: "built-in readers still work when custom reader does not match", + objManifests: []string{jobCompleteManifest}, + customReader: &mockStatusReader{ + supportedGK: v1.SchemeGroupVersion.WithKind("Pod").GroupKind(), + status: status.InProgressStatus, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + c := newTestClient(t) + fakeClient := dynamicfake.NewSimpleDynamicClient(scheme.Scheme) + fakeMapper := testutil.NewFakeRESTMapper( + v1.SchemeGroupVersion.WithKind("Pod"), + batchv1.SchemeGroupVersion.WithKind("Job"), + ) + statusWaiter := statusWaiter{ + client: fakeClient, + restMapper: fakeMapper, + readers: []engine.StatusReader{tt.customReader}, + } + objs := getRuntimeObjFromManifests(t, tt.objManifests) + for _, obj := range objs { + u := obj.(*unstructured.Unstructured) + gvr := getGVR(t, fakeMapper, u) + err := fakeClient.Tracker().Create(gvr, u, u.GetNamespace()) + assert.NoError(t, err) + } + resourceList := getResourceListFromRuntimeObjs(t, c, objs) + err := statusWaiter.WatchUntilReady(resourceList, time.Second*3) + if tt.expectErrStrs != nil { + require.Error(t, err) + for _, expectedErrStr := range tt.expectErrStrs { + assert.Contains(t, err.Error(), expectedErrStr) + } + return + } + assert.NoError(t, err) + }) + } +} diff --git a/pkg/helm/pkg/kube/wait.go b/pkg/helm/pkg/kube/wait.go index ecdd3894..747291a9 100644 --- a/pkg/helm/pkg/kube/wait.go +++ b/pkg/helm/pkg/kube/wait.go @@ -14,14 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kube // import "helm.sh/helm/v3/pkg/kube" +package kube // import "github.com/werf/nelm/pkg/helm/pkg/kube" import ( "context" "fmt" + "log/slog" + "net/http" "time" - "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" appsv1beta1 "k8s.io/api/apps/v1beta1" appsv1beta2 "k8s.io/api/apps/v1beta2" @@ -30,30 +31,66 @@ import ( extensionsv1beta1 "k8s.io/api/extensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes" + cachetools "k8s.io/client-go/tools/cache" + watchtools "k8s.io/client-go/tools/watch" "k8s.io/apimachinery/pkg/util/wait" ) -type waiter struct { - c ReadyChecker - timeout time.Duration - log func(string, ...interface{}) +// legacyWaiter is the legacy implementation of the Waiter interface. This logic was used by default in Helm 3 +// Helm 4 now uses the StatusWaiter implementation instead +type legacyWaiter struct { + c ReadyChecker + kubeClient *kubernetes.Clientset + ctx context.Context +} + +func (hw *legacyWaiter) Wait(resources ResourceList, timeout time.Duration) error { + hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true)) + return hw.waitForResources(resources, timeout) +} + +func (hw *legacyWaiter) WaitWithJobs(resources ResourceList, timeout time.Duration) error { + hw.c = NewReadyChecker(hw.kubeClient, PausedAsReady(true), CheckJobs(true)) + return hw.waitForResources(resources, timeout) } // waitForResources polls to get the current status of all pods, PVCs, Services and // Jobs(optional) until all are ready or a timeout is reached -func (w *waiter) waitForResources(created ResourceList) error { - w.log("beginning wait for %d resources with timeout of %v", len(created), w.timeout) +func (hw *legacyWaiter) waitForResources(created ResourceList, timeout time.Duration) error { + slog.Debug("beginning wait for resources", "count", len(created), "timeout", timeout) - ctx, cancel := context.WithTimeout(context.Background(), w.timeout) + ctx, cancel := hw.contextWithTimeout(timeout) defer cancel() + numberOfErrors := make([]int, len(created)) + for i := range numberOfErrors { + numberOfErrors[i] = 0 + } + return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { - for _, v := range created { - ready, err := w.c.IsReady(ctx, v) - if !ready || err != nil { + waitRetries := 30 + for i, v := range created { + ready, err := hw.c.IsReady(ctx, v) + + if waitRetries > 0 && hw.isRetryableError(err, v) { + numberOfErrors[i]++ + if numberOfErrors[i] > waitRetries { + slog.Debug("max number of retries reached", "resource", v.Name, "retries", numberOfErrors[i]) + return false, err + } + slog.Debug("retrying resource readiness", "resource", v.Name, "currentRetries", numberOfErrors[i]-1, "maxRetries", waitRetries) + return false, nil + } + numberOfErrors[i] = 0 + if !ready { return false, err } } @@ -61,14 +98,43 @@ func (w *waiter) waitForResources(created ResourceList) error { }) } -// waitForDeletedResources polls to check if all the resources are deleted or a timeout is reached -func (w *waiter) waitForDeletedResources(deleted ResourceList) error { - w.log("beginning wait for %d resources to be deleted with timeout of %v", len(deleted), w.timeout) +func (hw *legacyWaiter) isRetryableError(err error, resource *resource.Info) bool { + if err == nil { + return false + } + slog.Debug( + "error received when checking resource status", + slog.String("resource", resource.Name), + slog.Any("error", err), + ) + if ev, ok := err.(*apierrors.StatusError); ok { + statusCode := ev.Status().Code + retryable := hw.isRetryableHTTPStatusCode(statusCode) + slog.Debug( + "status code received", + slog.String("resource", resource.Name), + slog.Int("statusCode", int(statusCode)), + slog.Bool("retryable", retryable), + ) + return retryable + } + slog.Debug("retryable error assumed", "resource", resource.Name) + return true +} + +func (hw *legacyWaiter) isRetryableHTTPStatusCode(httpStatusCode int32) bool { + return httpStatusCode == 0 || httpStatusCode == http.StatusTooManyRequests || (httpStatusCode >= 500 && httpStatusCode != http.StatusNotImplemented) +} + +// WaitForDelete polls to check if all the resources are deleted or a timeout is reached +func (hw *legacyWaiter) WaitForDelete(deleted ResourceList, timeout time.Duration) error { + slog.Debug("beginning wait for resources to be deleted", "count", len(deleted), "timeout", timeout) - ctx, cancel := context.WithTimeout(context.Background(), w.timeout) + startTime := time.Now() + ctx, cancel := hw.contextWithTimeout(timeout) defer cancel() - return wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(ctx context.Context) (bool, error) { + err := wait.PollUntilContextCancel(ctx, 2*time.Second, true, func(_ context.Context) (bool, error) { for _, v := range deleted { err := v.Get() if err == nil || !apierrors.IsNotFound(err) { @@ -77,6 +143,15 @@ func (w *waiter) waitForDeletedResources(deleted ResourceList) error { } return true, nil }) + + elapsed := time.Since(startTime).Round(time.Second) + if err != nil { + slog.Debug("wait for resources failed", slog.Duration("elapsed", elapsed), slog.Any("error", err)) + } else { + slog.Debug("wait for resources succeeded", slog.Duration("elapsed", elapsed)) + } + + return err } // SelectorsForObject returns the pod label selector for a given object @@ -115,7 +190,7 @@ func SelectorsForObject(object runtime.Object) (selector labels.Selector, err er case *batchv1.Job: selector, err = metav1.LabelSelectorAsSelector(t.Spec.Selector) case *corev1.Service: - if t.Spec.Selector == nil || len(t.Spec.Selector) == 0 { + if len(t.Spec.Selector) == 0 { return nil, fmt.Errorf("invalid service '%s': Service is defined without a selector", t.Name) } selector = labels.SelectorFromSet(t.Spec.Selector) @@ -124,5 +199,147 @@ func SelectorsForObject(object runtime.Object) (selector labels.Selector, err er return nil, fmt.Errorf("selector for %T not implemented", object) } - return selector, errors.Wrap(err, "invalid label selector") + if err != nil { + return selector, fmt.Errorf("invalid label selector: %w", err) + } + + return selector, nil +} + +func (hw *legacyWaiter) watchTimeout(t time.Duration) func(*resource.Info) error { + return func(info *resource.Info) error { + return hw.watchUntilReady(t, info) + } +} + +// WatchUntilReady watches the resources given and waits until it is ready. +// +// This method is mainly for hook implementations. It watches for a resource to +// hit a particular milestone. The milestone depends on the Kind. +// +// For most kinds, it checks to see if the resource is marked as Added or Modified +// by the Kubernetes event stream. For some kinds, it does more: +// +// - Jobs: A job is marked "Ready" when it has successfully completed. This is +// ascertained by watching the Status fields in a job's output. +// - Pods: A pod is marked "Ready" when it has successfully completed. This is +// ascertained by watching the status.phase field in a pod's output. +// +// Handling for other kinds will be added as necessary. +func (hw *legacyWaiter) WatchUntilReady(resources ResourceList, timeout time.Duration) error { + // For jobs, there's also the option to do poll c.Jobs(namespace).Get(): + // https://github.com/adamreese/kubernetes/blob/master/test/e2e/job.go#L291-L300 + return perform(resources, hw.watchTimeout(timeout)) +} + +func (hw *legacyWaiter) watchUntilReady(timeout time.Duration, info *resource.Info) error { + kind := info.Mapping.GroupVersionKind.Kind + switch kind { + case "Job", "Pod": + default: + return nil + } + + slog.Debug("watching for resource changes", "kind", kind, "resource", info.Name, "timeout", timeout) + + // Use a selector on the name of the resource. This should be unique for the + // given version and kind + selector, err := fields.ParseSelector(fmt.Sprintf("metadata.name=%s", info.Name)) + if err != nil { + return err + } + lw := cachetools.NewListWatchFromClient(info.Client, info.Mapping.Resource.Resource, info.Namespace, selector) + + // What we watch for depends on the Kind. + // - For a Job, we watch for completion. + // - For all else, we watch until Ready. + // In the future, we might want to add some special logic for types + // like Ingress, Volume, etc. + + ctx, cancel := hw.contextWithTimeout(timeout) + defer cancel() + _, err = watchtools.UntilWithSync(ctx, lw, &unstructured.Unstructured{}, nil, func(e watch.Event) (bool, error) { + // Make sure the incoming object is versioned as we use unstructured + // objects when we build manifests + obj := convertWithMapper(e.Object, info.Mapping) + switch e.Type { + case watch.Added, watch.Modified: + // For things like a secret or a config map, this is the best indicator + // we get. We care mostly about jobs, where what we want to see is + // the status go into a good state. For other types, like ReplicaSet + // we don't really do anything to support these as hooks. + slog.Debug("add/modify event received", "resource", info.Name, "eventType", e.Type) + + switch kind { + case "Job": + return hw.waitForJob(obj, info.Name) + case "Pod": + return hw.waitForPodSuccess(obj, info.Name) + } + return true, nil + case watch.Deleted: + slog.Debug("deleted event received", "resource", info.Name) + return true, nil + case watch.Error: + // Handle error and return with an error. + slog.Error("error event received", "resource", info.Name) + return true, fmt.Errorf("failed to deploy %s", info.Name) + default: + return false, nil + } + }) + return err +} + +// waitForJob is a helper that waits for a job to complete. +// +// This operates on an event returned from a watcher. +func (hw *legacyWaiter) waitForJob(obj runtime.Object, name string) (bool, error) { + o, ok := obj.(*batchv1.Job) + if !ok { + return true, fmt.Errorf("expected %s to be a *batch.Job, got %T", name, obj) + } + + for _, c := range o.Status.Conditions { + if c.Type == batchv1.JobComplete && c.Status == "True" { + return true, nil + } else if c.Type == batchv1.JobFailed && c.Status == "True" { + slog.Error("job failed", "job", name, "reason", c.Reason) + return true, fmt.Errorf("job %s failed: %s", name, c.Reason) + } + } + + slog.Debug("job status update", "job", name, "active", o.Status.Active, "failed", o.Status.Failed, "succeeded", o.Status.Succeeded) + return false, nil +} + +// waitForPodSuccess is a helper that waits for a pod to complete. +// +// This operates on an event returned from a watcher. +func (hw *legacyWaiter) waitForPodSuccess(obj runtime.Object, name string) (bool, error) { + o, ok := obj.(*corev1.Pod) + if !ok { + return true, fmt.Errorf("expected %s to be a *v1.Pod, got %T", name, obj) + } + + switch o.Status.Phase { + case corev1.PodSucceeded: + slog.Debug("pod succeeded", "pod", o.Name) + return true, nil + case corev1.PodFailed: + slog.Error("pod failed", "pod", o.Name) + return true, fmt.Errorf("pod %s failed", o.Name) + case corev1.PodPending: + slog.Debug("pod pending", "pod", o.Name) + case corev1.PodRunning: + slog.Debug("pod running", "pod", o.Name) + case corev1.PodUnknown: + slog.Debug("pod unknown", "pod", o.Name) + } + + return false, nil +} + +func (hw *legacyWaiter) contextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) { + return contextWithTimeout(hw.ctx, timeout) } diff --git a/pkg/helm/pkg/kube/wait_test.go b/pkg/helm/pkg/kube/wait_test.go new file mode 100644 index 00000000..d96f2c48 --- /dev/null +++ b/pkg/helm/pkg/kube/wait_test.go @@ -0,0 +1,467 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kube + +import ( + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + appsv1beta1 "k8s.io/api/apps/v1beta1" + appsv1beta2 "k8s.io/api/apps/v1beta2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/resource" +) + +func TestSelectorsForObject(t *testing.T) { + tests := []struct { + name string + object interface{} + expectError bool + errorContains string + expectedLabels map[string]string + }{ + { + name: "appsv1 ReplicaSet", + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + }, + }, + expectError: false, + expectedLabels: map[string]string{"app": "test"}, + }, + { + name: "extensionsv1beta1 ReplicaSet", + object: &extensionsv1beta1.ReplicaSet{ + Spec: extensionsv1beta1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "ext-rs"}}, + }, + }, + expectedLabels: map[string]string{"app": "ext-rs"}, + }, + { + name: "appsv1beta2 ReplicaSet", + object: &appsv1beta2.ReplicaSet{ + Spec: appsv1beta2.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "beta2-rs"}}, + }, + }, + expectedLabels: map[string]string{"app": "beta2-rs"}, + }, + { + name: "corev1 ReplicationController", + object: &corev1.ReplicationController{ + Spec: corev1.ReplicationControllerSpec{ + Selector: map[string]string{"rc": "test"}, + }, + }, + expectError: false, + expectedLabels: map[string]string{"rc": "test"}, + }, + { + name: "appsv1 StatefulSet", + object: &appsv1.StatefulSet{ + Spec: appsv1.StatefulSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "statefulset-v1"}}, + }, + }, + expectedLabels: map[string]string{"app": "statefulset-v1"}, + }, + { + name: "appsv1beta1 StatefulSet", + object: &appsv1beta1.StatefulSet{ + Spec: appsv1beta1.StatefulSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "statefulset-beta1"}}, + }, + }, + expectedLabels: map[string]string{"app": "statefulset-beta1"}, + }, + { + name: "appsv1beta2 StatefulSet", + object: &appsv1beta2.StatefulSet{ + Spec: appsv1beta2.StatefulSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "statefulset-beta2"}}, + }, + }, + expectedLabels: map[string]string{"app": "statefulset-beta2"}, + }, + { + name: "extensionsv1beta1 DaemonSet", + object: &extensionsv1beta1.DaemonSet{ + Spec: extensionsv1beta1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "daemonset-ext-beta1"}}, + }, + }, + expectedLabels: map[string]string{"app": "daemonset-ext-beta1"}, + }, + { + name: "appsv1 DaemonSet", + object: &appsv1.DaemonSet{ + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "daemonset-v1"}}, + }, + }, + expectedLabels: map[string]string{"app": "daemonset-v1"}, + }, + { + name: "appsv1beta2 DaemonSet", + object: &appsv1beta2.DaemonSet{ + Spec: appsv1beta2.DaemonSetSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "daemonset-beta2"}}, + }, + }, + expectedLabels: map[string]string{"app": "daemonset-beta2"}, + }, + { + name: "extensionsv1beta1 Deployment", + object: &extensionsv1beta1.Deployment{ + Spec: extensionsv1beta1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "deployment-ext-beta1"}}, + }, + }, + expectedLabels: map[string]string{"app": "deployment-ext-beta1"}, + }, + { + name: "appsv1 Deployment", + object: &appsv1.Deployment{ + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "deployment-v1"}}, + }, + }, + expectedLabels: map[string]string{"app": "deployment-v1"}, + }, + { + name: "appsv1beta1 Deployment", + object: &appsv1beta1.Deployment{ + Spec: appsv1beta1.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "deployment-beta1"}}, + }, + }, + expectedLabels: map[string]string{"app": "deployment-beta1"}, + }, + { + name: "appsv1beta2 Deployment", + object: &appsv1beta2.Deployment{ + Spec: appsv1beta2.DeploymentSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "deployment-beta2"}}, + }, + }, + expectedLabels: map[string]string{"app": "deployment-beta2"}, + }, + { + name: "batchv1 Job", + object: &batchv1.Job{ + Spec: batchv1.JobSpec{ + Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"job": "batch-job"}}, + }, + }, + expectedLabels: map[string]string{"job": "batch-job"}, + }, + { + name: "corev1 Service with selector", + object: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "svc"}, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{"svc": "yes"}, + }, + }, + expectError: false, + expectedLabels: map[string]string{"svc": "yes"}, + }, + { + name: "corev1 Service without selector", + object: &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{Name: "svc"}, + Spec: corev1.ServiceSpec{Selector: map[string]string{}}, + }, + expectError: true, + errorContains: "invalid service 'svc': Service is defined without a selector", + }, + { + name: "invalid label selector", + object: &appsv1.ReplicaSet{ + Spec: appsv1.ReplicaSetSpec{ + Selector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: "foo", + Operator: "InvalidOperator", + Values: []string{"bar"}, + }, + }, + }, + }, + }, + expectError: true, + errorContains: "invalid label selector:", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + selector, err := SelectorsForObject(tt.object.(runtime.Object)) + if tt.expectError { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errorContains) + } else { + assert.NoError(t, err) + expected := labels.Set(tt.expectedLabels) + assert.True(t, selector.Matches(expected), "expected selector to match") + } + }) + } +} + +func TestLegacyWaiter_waitForPodSuccess(t *testing.T) { + lw := &legacyWaiter{} + + tests := []struct { + name string + obj runtime.Object + wantDone bool + wantErr bool + errMessage string + }{ + { + name: "pod succeeded", + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod1"}, + Status: corev1.PodStatus{Phase: corev1.PodSucceeded}, + }, + wantDone: true, + wantErr: false, + }, + { + name: "pod failed", + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod2"}, + Status: corev1.PodStatus{Phase: corev1.PodFailed}, + }, + wantDone: true, + wantErr: true, + errMessage: "pod pod2 failed", + }, + { + name: "pod pending", + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod3"}, + Status: corev1.PodStatus{Phase: corev1.PodPending}, + }, + wantDone: false, + wantErr: false, + }, + { + name: "pod running", + obj: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{Name: "pod4"}, + Status: corev1.PodStatus{Phase: corev1.PodRunning}, + }, + wantDone: false, + wantErr: false, + }, + { + name: "wrong object type", + obj: &metav1.Status{}, + wantDone: true, + wantErr: true, + errMessage: "expected foo to be a *v1.Pod, got *v1.Status", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + done, err := lw.waitForPodSuccess(tt.obj, "foo") + if tt.wantErr { + if err == nil { + t.Errorf("expected error, got none") + } else if !strings.Contains(err.Error(), tt.errMessage) { + t.Errorf("expected error to contain %q, got %q", tt.errMessage, err.Error()) + } + } else if err != nil { + t.Errorf("unexpected error: %v", err) + } + if done != tt.wantDone { + t.Errorf("got done=%v, want %v", done, tt.wantDone) + } + }) + } +} + +func TestLegacyWaiter_waitForJob(t *testing.T) { + lw := &legacyWaiter{} + + tests := []struct { + name string + obj runtime.Object + wantDone bool + wantErr bool + errMessage string + }{ + { + name: "job complete", + obj: &batchv1.Job{ + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: "True", + }, + }, + }, + }, + wantDone: true, + wantErr: false, + }, + { + name: "job failed", + obj: &batchv1.Job{ + Status: batchv1.JobStatus{ + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobFailed, + Status: "True", + Reason: "FailedReason", + }, + }, + }, + }, + wantDone: true, + wantErr: true, + errMessage: "job test-job failed: FailedReason", + }, + { + name: "job in progress", + obj: &batchv1.Job{ + Status: batchv1.JobStatus{ + Active: 1, + Failed: 0, + Succeeded: 0, + Conditions: []batchv1.JobCondition{ + { + Type: batchv1.JobComplete, + Status: "False", + }, + { + Type: batchv1.JobFailed, + Status: "False", + }, + }, + }, + }, + wantDone: false, + wantErr: false, + }, + { + name: "wrong object type", + obj: &metav1.Status{}, + wantDone: true, + wantErr: true, + errMessage: "expected test-job to be a *batch.Job, got *v1.Status", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + done, err := lw.waitForJob(tt.obj, "test-job") + if tt.wantErr { + if err == nil { + t.Errorf("expected error, got none") + } else if !strings.Contains(err.Error(), tt.errMessage) { + t.Errorf("expected error to contain %q, got %q", tt.errMessage, err.Error()) + } + } else if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if done != tt.wantDone { + t.Errorf("got done=%v, want %v", done, tt.wantDone) + } + }) + } +} + +func TestLegacyWaiter_isRetryableError(t *testing.T) { + lw := &legacyWaiter{} + + info := &resource.Info{ + Name: "test-resource", + } + + tests := []struct { + name string + err error + wantRetry bool + description string + }{ + { + name: "nil error", + err: nil, + wantRetry: false, + }, + { + name: "status error - 0 code", + err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: 0}}, + wantRetry: true, + }, + { + name: "status error - 429 (TooManyRequests)", + err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusTooManyRequests}}, + wantRetry: true, + }, + { + name: "status error - 503", + err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusServiceUnavailable}}, + wantRetry: true, + }, + { + name: "status error - 501 (NotImplemented)", + err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusNotImplemented}}, + wantRetry: false, + }, + { + name: "status error - 400 (Bad Request)", + err: &apierrors.StatusError{ErrStatus: metav1.Status{Code: http.StatusBadRequest}}, + wantRetry: false, + }, + { + name: "non-status error", + err: fmt.Errorf("some generic error"), + wantRetry: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := lw.isRetryableError(tt.err, info) + if got != tt.wantRetry { + t.Errorf("isRetryableError() = %v, want %v", got, tt.wantRetry) + } + }) + } +} diff --git a/pkg/helm/pkg/lint/lint.go b/pkg/helm/pkg/lint/lint.go deleted file mode 100644 index e2c33952..00000000 --- a/pkg/helm/pkg/lint/lint.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package lint // import "helm.sh/helm/v3/pkg/lint" - -import ( - "path/filepath" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/rules" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -// All runs all of the available linters on the given base directory. -func All(basedir string, values map[string]interface{}, namespace string, _ bool, opts helmopts.HelmOptions) support.Linter { - return AllWithKubeVersion(basedir, values, namespace, nil, opts) -} - -// AllWithKubeVersion runs all the available linters on the given base directory, allowing to specify the kubernetes version. -func AllWithKubeVersion(basedir string, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, opts helmopts.HelmOptions) support.Linter { - // Using abs path to get directory context - chartDir, _ := filepath.Abs(basedir) - - linter := support.Linter{ChartDir: chartDir} - if false { - rules.Chartfile(&linter) - } - rules.ValuesWithOverrides(&linter, values) - rules.TemplatesWithKubeVersion(&linter, values, namespace, kubeVersion, opts) - rules.Dependencies(&linter, opts) - return linter -} diff --git a/pkg/helm/pkg/lint/lint_test.go b/pkg/helm/pkg/lint/lint_test.go deleted file mode 100644 index 79baea3c..00000000 --- a/pkg/helm/pkg/lint/lint_test.go +++ /dev/null @@ -1,173 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package lint - -import ( - "strings" - "testing" - "time" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" -) - -var values map[string]interface{} - -const namespace = "testNamespace" -const strict = false - -const badChartDir = "rules/testdata/badchartfile" -const badValuesFileDir = "rules/testdata/badvaluesfile" -const badYamlFileDir = "rules/testdata/albatross" -const goodChartDir = "rules/testdata/goodone" -const subChartValuesDir = "rules/testdata/withsubchart" -const malformedTemplate = "rules/testdata/malformed-template" - -func TestBadChart(t *testing.T) { - m := All(badChartDir, values, namespace, strict).Messages - if len(m) != 8 { - t.Errorf("Number of errors %v", len(m)) - t.Errorf("All didn't fail with expected errors, got %#v", m) - } - // There should be one INFO, and 2 ERROR messages, check for them - var i, e, e2, e3, e4, e5, e6 bool - for _, msg := range m { - if msg.Severity == support.InfoSev { - if strings.Contains(msg.Err.Error(), "icon is recommended") { - i = true - } - } - if msg.Severity == support.ErrorSev { - if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { - e = true - } - if strings.Contains(msg.Err.Error(), "name is required") { - e2 = true - } - - if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { - e3 = true - } - - if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { - e4 = true - } - - if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { - e5 = true - } - // This comes from the dependency check, which loads dependency info from the Chart.yaml - if strings.Contains(msg.Err.Error(), "unable to load chart") { - e6 = true - } - } - } - if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 { - t.Errorf("Didn't find all the expected errors, got %#v", m) - } -} - -func TestInvalidYaml(t *testing.T) { - m := All(badYamlFileDir, values, namespace, strict).Messages - if len(m) != 1 { - t.Fatalf("All didn't fail with expected errors, got %#v", m) - } - if !strings.Contains(m[0].Err.Error(), "deliberateSyntaxError") { - t.Errorf("All didn't have the error for deliberateSyntaxError") - } -} - -func TestBadValues(t *testing.T) { - m := All(badValuesFileDir, values, namespace, strict).Messages - if len(m) < 1 { - t.Fatalf("All didn't fail with expected errors, got %#v", m) - } - if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { - t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) - } -} - -func TestGoodChart(t *testing.T) { - m := All(goodChartDir, values, namespace, strict).Messages - if len(m) != 0 { - t.Error("All returned linter messages when it shouldn't have") - for i, msg := range m { - t.Logf("Message %d: %s", i, msg) - } - } -} - -// TestHelmCreateChart tests that a `helm create` always passes a `helm lint` test. -// -// See https://github.com/helm/helm/issues/7923 -func TestHelmCreateChart(t *testing.T) { - dir := t.TempDir() - - createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) - if err != nil { - t.Error(err) - // Fatal is bad because of the defer. - return - } - - // Note: we test with strict=true here, even though others have - // strict = false. - m := All(createdChart, values, namespace, true).Messages - if ll := len(m); ll != 1 { - t.Errorf("All should have had exactly 1 error. Got %d", ll) - for i, msg := range m { - t.Logf("Message %d: %s", i, msg.Error()) - } - } else if msg := m[0].Err.Error(); !strings.Contains(msg, "icon is recommended") { - t.Errorf("Unexpected lint error: %s", msg) - } -} - -// lint ignores import-values -// See https://github.com/helm/helm/issues/9658 -func TestSubChartValuesChart(t *testing.T) { - m := All(subChartValuesDir, values, namespace, strict).Messages - if len(m) != 0 { - t.Error("All returned linter messages when it shouldn't have") - for i, msg := range m { - t.Logf("Message %d: %s", i, msg) - } - } -} - -// lint stuck with malformed template object -// See https://github.com/helm/helm/issues/11391 -func TestMalformedTemplate(t *testing.T) { - c := time.After(3 * time.Second) - ch := make(chan int, 1) - var m []support.Message - go func() { - m = All(malformedTemplate, values, namespace, strict).Messages - ch <- 1 - }() - select { - case <-c: - t.Fatalf("lint malformed template timeout") - case <-ch: - if len(m) != 1 { - t.Fatalf("All didn't fail with expected errors, got %#v", m) - } - if !strings.Contains(m[0].Err.Error(), "invalid character '{'") { - t.Errorf("All didn't have the error for invalid character '{'") - } - } -} diff --git a/pkg/helm/pkg/lint/rules/chartfile.go b/pkg/helm/pkg/lint/rules/chartfile.go deleted file mode 100644 index 83d2585b..00000000 --- a/pkg/helm/pkg/lint/rules/chartfile.go +++ /dev/null @@ -1,213 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules // import "helm.sh/helm/v3/pkg/lint/rules" - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/Masterminds/semver/v3" - "github.com/asaskevich/govalidator" - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" -) - -// Chartfile runs a set of linter rules related to Chart.yaml file -func Chartfile(linter *support.Linter) { - chartFileName := "Chart.yaml" - chartPath := filepath.Join(linter.ChartDir, chartFileName) - - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlNotDirectory(chartPath)) - - chartFile, err := chartutil.LoadChartfile(chartPath) - validChartFile := linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartYamlFormat(err)) - - // Guard clause. Following linter rules require a parsable ChartFile - if !validChartFile { - return - } - - // type check for Chart.yaml . ignoring error as any parse - // errors would already be caught in the above load function - chartFileForTypeCheck, _ := loadChartFileForTypeCheck(chartPath) - - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) - - // Chart metadata - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) - - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersionType(chartFileForTypeCheck)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartVersion(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAppVersionType(chartFileForTypeCheck)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartMaintainer(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartSources(chartFile)) - linter.RunLinterRule(support.InfoSev, chartFileName, validateChartIconPresence(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) -} - -func validateChartVersionType(data map[string]interface{}) error { - return isStringValue(data, "version") -} - -func validateChartAppVersionType(data map[string]interface{}) error { - return isStringValue(data, "appVersion") -} - -func isStringValue(data map[string]interface{}, key string) error { - value, ok := data[key] - if !ok { - return nil - } - valueType := fmt.Sprintf("%T", value) - if valueType != "string" { - return errors.Errorf("%s should be of type string but it's of type %s", key, valueType) - } - return nil -} - -func validateChartYamlNotDirectory(chartPath string) error { - fi, err := os.Stat(chartPath) - - if err == nil && fi.IsDir() { - return errors.New("should be a file, not a directory") - } - return nil -} - -func validateChartYamlFormat(chartFileError error) error { - if chartFileError != nil { - return errors.Errorf("unable to parse YAML\n\t%s", chartFileError.Error()) - } - return nil -} - -func validateChartName(cf *chart.Metadata) error { - if cf.Name == "" { - return errors.New("name is required") - } - name := filepath.Base(cf.Name) - if name != cf.Name { - return fmt.Errorf("chart name %q is invalid", cf.Name) - } - return nil -} - -func validateChartAPIVersion(cf *chart.Metadata) error { - if cf.APIVersion == "" { - return errors.New("apiVersion is required. The value must be either \"v1\" or \"v2\"") - } - - if cf.APIVersion != chart.APIVersionV1 && cf.APIVersion != chart.APIVersionV2 { - return fmt.Errorf("apiVersion '%s' is not valid. The value must be either \"v1\" or \"v2\"", cf.APIVersion) - } - - return nil -} - -func validateChartVersion(cf *chart.Metadata) error { - if cf.Version == "" { - return errors.New("version is required") - } - - version, err := semver.NewVersion(cf.Version) - - if err != nil { - return errors.Errorf("version '%s' is not a valid SemVer", cf.Version) - } - - c, err := semver.NewConstraint(">0.0.0-0") - if err != nil { - return err - } - valid, msg := c.Validate(version) - - if !valid && len(msg) > 0 { - return errors.Errorf("version %v", msg[0]) - } - - return nil -} - -func validateChartMaintainer(cf *chart.Metadata) error { - for _, maintainer := range cf.Maintainers { - if maintainer.Name == "" { - return errors.New("each maintainer requires a name") - } else if maintainer.Email != "" && !govalidator.IsEmail(maintainer.Email) { - return errors.Errorf("invalid email '%s' for maintainer '%s'", maintainer.Email, maintainer.Name) - } else if maintainer.URL != "" && !govalidator.IsURL(maintainer.URL) { - return errors.Errorf("invalid url '%s' for maintainer '%s'", maintainer.URL, maintainer.Name) - } - } - return nil -} - -func validateChartSources(cf *chart.Metadata) error { - for _, source := range cf.Sources { - if source == "" || !govalidator.IsRequestURL(source) { - return errors.Errorf("invalid source URL '%s'", source) - } - } - return nil -} - -func validateChartIconPresence(cf *chart.Metadata) error { - if cf.Icon == "" { - return errors.New("icon is recommended") - } - return nil -} - -func validateChartIconURL(cf *chart.Metadata) error { - if cf.Icon != "" && !govalidator.IsRequestURL(cf.Icon) { - return errors.Errorf("invalid icon URL '%s'", cf.Icon) - } - return nil -} - -func validateChartDependencies(cf *chart.Metadata) error { - if len(cf.Dependencies) > 0 && cf.APIVersion != chart.APIVersionV2 { - return fmt.Errorf("dependencies are not valid in the Chart file with apiVersion '%s'. They are valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) - } - return nil -} - -func validateChartType(cf *chart.Metadata) error { - if len(cf.Type) > 0 && cf.APIVersion != chart.APIVersionV2 { - return fmt.Errorf("chart type is not valid in apiVersion '%s'. It is valid in apiVersion '%s'", cf.APIVersion, chart.APIVersionV2) - } - return nil -} - -// loadChartFileForTypeCheck loads the Chart.yaml -// in a generic form of a map[string]interface{}, so that the type -// of the values can be checked -func loadChartFileForTypeCheck(filename string) (map[string]interface{}, error) { - b, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - y := make(map[string]interface{}) - err = yaml.Unmarshal(b, &y) - return y, err -} diff --git a/pkg/helm/pkg/lint/rules/chartfile_test.go b/pkg/helm/pkg/lint/rules/chartfile_test.go deleted file mode 100644 index b98cf3eb..00000000 --- a/pkg/helm/pkg/lint/rules/chartfile_test.go +++ /dev/null @@ -1,255 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" -) - -const ( - badCharNametDir = "testdata/badchartname" - badChartDir = "testdata/badchartfile" - anotherBadChartDir = "testdata/anotherbadchartfile" -) - -var ( - badChartNamePath = filepath.Join(badCharNametDir, "Chart.yaml") - badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") - nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") -) - -var badChart, _ = chartutil.LoadChartfile(badChartFilePath) -var badChartName, _ = chartutil.LoadChartfile(badChartNamePath) - -// Validation functions Test -func TestValidateChartYamlNotDirectory(t *testing.T) { - _ = os.Mkdir(nonExistingChartFilePath, os.ModePerm) - defer os.Remove(nonExistingChartFilePath) - - err := validateChartYamlNotDirectory(nonExistingChartFilePath) - if err == nil { - t.Errorf("validateChartYamlNotDirectory to return a linter error, got no error") - } -} - -func TestValidateChartYamlFormat(t *testing.T) { - err := validateChartYamlFormat(errors.New("Read error")) - if err == nil { - t.Errorf("validateChartYamlFormat to return a linter error, got no error") - } - - err = validateChartYamlFormat(nil) - if err != nil { - t.Errorf("validateChartYamlFormat to return no error, got a linter error") - } -} - -func TestValidateChartName(t *testing.T) { - err := validateChartName(badChart) - if err == nil { - t.Errorf("validateChartName to return a linter error, got no error") - } - - err = validateChartName(badChartName) - if err == nil { - t.Error("expected validateChartName to return a linter error for an invalid name, got no error") - } -} - -func TestValidateChartVersion(t *testing.T) { - var failTest = []struct { - Version string - ErrorMsg string - }{ - {"", "version is required"}, - {"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"}, - {"waps", "'waps' is not a valid SemVer"}, - {"-3", "'-3' is not a valid SemVer"}, - } - - var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} - - for _, test := range failTest { - badChart.Version = test.Version - err := validateChartVersion(badChart) - if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { - t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) - } - } - - for _, version := range successTest { - badChart.Version = version - err := validateChartVersion(badChart) - if err != nil { - t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) - } - } -} - -func TestValidateChartMaintainer(t *testing.T) { - var failTest = []struct { - Name string - Email string - ErrorMsg string - }{ - {"", "", "each maintainer requires a name"}, - {"", "test@test.com", "each maintainer requires a name"}, - {"John Snow", "wrongFormatEmail.com", "invalid email"}, - } - - var successTest = []struct { - Name string - Email string - }{ - {"John Snow", ""}, - {"John Snow", "john@winterfell.com"}, - } - - for _, test := range failTest { - badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} - err := validateChartMaintainer(badChart) - if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { - t.Errorf("validateChartMaintainer(%s, %s) to return \"%s\", got no error", test.Name, test.Email, test.ErrorMsg) - } - } - - for _, test := range successTest { - badChart.Maintainers = []*chart.Maintainer{{Name: test.Name, Email: test.Email}} - err := validateChartMaintainer(badChart) - if err != nil { - t.Errorf("validateChartMaintainer(%s, %s) to return no error, got %s", test.Name, test.Email, err.Error()) - } - } -} - -func TestValidateChartSources(t *testing.T) { - var failTest = []string{"", "RiverRun", "john@winterfell", "riverrun.io"} - var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish"} - for _, test := range failTest { - badChart.Sources = []string{test} - err := validateChartSources(badChart) - if err == nil || !strings.Contains(err.Error(), "invalid source URL") { - t.Errorf("validateChartSources(%s) to return \"invalid source URL\", got no error", test) - } - } - - for _, test := range successTest { - badChart.Sources = []string{test} - err := validateChartSources(badChart) - if err != nil { - t.Errorf("validateChartSources(%s) to return no error, got %s", test, err.Error()) - } - } -} - -func TestValidateChartIconPresence(t *testing.T) { - err := validateChartIconPresence(badChart) - if err == nil { - t.Errorf("validateChartIconPresence to return a linter error, got no error") - } -} - -func TestValidateChartIconURL(t *testing.T) { - var failTest = []string{"RiverRun", "john@winterfell", "riverrun.io"} - var successTest = []string{"http://riverrun.io", "https://riverrun.io", "https://riverrun.io/blackfish.png"} - for _, test := range failTest { - badChart.Icon = test - err := validateChartIconURL(badChart) - if err == nil || !strings.Contains(err.Error(), "invalid icon URL") { - t.Errorf("validateChartIconURL(%s) to return \"invalid icon URL\", got no error", test) - } - } - - for _, test := range successTest { - badChart.Icon = test - err := validateChartSources(badChart) - if err != nil { - t.Errorf("validateChartIconURL(%s) to return no error, got %s", test, err.Error()) - } - } -} - -func TestChartfile(t *testing.T) { - t.Run("Chart.yaml basic validity issues", func(t *testing.T) { - linter := support.Linter{ChartDir: badChartDir} - Chartfile(&linter) - msgs := linter.Messages - expectedNumberOfErrorMessages := 6 - - if len(msgs) != expectedNumberOfErrorMessages { - t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) - return - } - - if !strings.Contains(msgs[0].Err.Error(), "name is required") { - t.Errorf("Unexpected message 0: %s", msgs[0].Err) - } - - if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { - t.Errorf("Unexpected message 1: %s", msgs[1].Err) - } - - if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { - t.Errorf("Unexpected message 2: %s", msgs[2].Err) - } - - if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { - t.Errorf("Unexpected message 3: %s", msgs[3].Err) - } - - if !strings.Contains(msgs[4].Err.Error(), "chart type is not valid in apiVersion") { - t.Errorf("Unexpected message 4: %s", msgs[4].Err) - } - - if !strings.Contains(msgs[5].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { - t.Errorf("Unexpected message 5: %s", msgs[5].Err) - } - }) - - t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { - linter := support.Linter{ChartDir: anotherBadChartDir} - Chartfile(&linter) - msgs := linter.Messages - expectedNumberOfErrorMessages := 3 - - if len(msgs) != expectedNumberOfErrorMessages { - t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) - return - } - - if !strings.Contains(msgs[0].Err.Error(), "version should be of type string") { - t.Errorf("Unexpected message 0: %s", msgs[0].Err) - } - - if !strings.Contains(msgs[1].Err.Error(), "version '7.2445e+06' is not a valid SemVer") { - t.Errorf("Unexpected message 1: %s", msgs[1].Err) - } - - if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { - t.Errorf("Unexpected message 2: %s", msgs[2].Err) - } - }) -} diff --git a/pkg/helm/pkg/lint/rules/dependencies.go b/pkg/helm/pkg/lint/rules/dependencies.go deleted file mode 100644 index d7e984ab..00000000 --- a/pkg/helm/pkg/lint/rules/dependencies.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules // import "helm.sh/helm/v3/pkg/lint/rules" - -import ( - "fmt" - "strings" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -// Dependencies runs lints against a chart's dependencies -// -// See https://github.com/helm/helm/issues/7910 -func Dependencies(linter *support.Linter, opts helmopts.HelmOptions) { - c, err := loader.LoadDir(linter.ChartDir, opts) - if !linter.RunLinterRule(support.ErrorSev, "", validateChartFormat(err)) { - return - } - - linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c)) - linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c)) - linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c)) -} - -func validateChartFormat(chartError error) error { - if chartError != nil { - return errors.Errorf("unable to load chart\n\t%s", chartError) - } - return nil -} - -func validateDependencyInChartsDir(c *chart.Chart) (err error) { - dependencies := map[string]struct{}{} - missing := []string{} - for _, dep := range c.Dependencies() { - dependencies[dep.Metadata.Name] = struct{}{} - } - for _, dep := range c.Metadata.Dependencies { - if _, ok := dependencies[dep.Name]; !ok { - missing = append(missing, dep.Name) - } - } - if len(missing) > 0 { - err = fmt.Errorf("chart directory is missing these dependencies: %s", strings.Join(missing, ",")) - } - return err -} - -func validateDependencyInMetadata(c *chart.Chart) (err error) { - dependencies := map[string]struct{}{} - missing := []string{} - for _, dep := range c.Metadata.Dependencies { - dependencies[dep.Name] = struct{}{} - } - for _, dep := range c.Dependencies() { - if _, ok := dependencies[dep.Metadata.Name]; !ok { - missing = append(missing, dep.Metadata.Name) - } - } - if len(missing) > 0 { - err = fmt.Errorf("chart metadata is missing these dependencies: %s", strings.Join(missing, ",")) - } - return err -} - -func validateDependenciesUnique(c *chart.Chart) (err error) { - dependencies := map[string]*chart.Dependency{} - shadowing := []string{} - - for _, dep := range c.Metadata.Dependencies { - key := dep.Name - if dep.Alias != "" { - key = dep.Alias - } - if dependencies[key] != nil { - shadowing = append(shadowing, key) - } - dependencies[key] = dep - } - if len(shadowing) > 0 { - err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ",")) - } - return err -} diff --git a/pkg/helm/pkg/lint/rules/dependencies_test.go b/pkg/helm/pkg/lint/rules/dependencies_test.go deleted file mode 100644 index cf197433..00000000 --- a/pkg/helm/pkg/lint/rules/dependencies_test.go +++ /dev/null @@ -1,157 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package rules - -import ( - "path/filepath" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" -) - -func chartWithBadDependencies() chart.Chart { - badChartDeps := chart.Chart{ - Metadata: &chart.Metadata{ - Name: "badchart", - Version: "0.1.0", - APIVersion: "v2", - Dependencies: []*chart.Dependency{ - { - Name: "sub2", - }, - { - Name: "sub3", - }, - }, - }, - } - - badChartDeps.SetDependencies( - &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "sub1", - Version: "0.1.0", - APIVersion: "v2", - }, - }, - &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "sub2", - Version: "0.1.0", - APIVersion: "v2", - }, - }, - ) - return badChartDeps -} - -func TestValidateDependencyInChartsDir(t *testing.T) { - c := chartWithBadDependencies() - - if err := validateDependencyInChartsDir(&c); err == nil { - t.Error("chart should have been flagged for missing deps in chart directory") - } -} - -func TestValidateDependencyInMetadata(t *testing.T) { - c := chartWithBadDependencies() - - if err := validateDependencyInMetadata(&c); err == nil { - t.Errorf("chart should have been flagged for missing deps in chart metadata") - } -} - -func TestValidateDependenciesUnique(t *testing.T) { - tests := []struct { - chart chart.Chart - }{ - {chart.Chart{ - Metadata: &chart.Metadata{ - Name: "badchart", - Version: "0.1.0", - APIVersion: "v2", - Dependencies: []*chart.Dependency{ - { - Name: "foo", - }, - { - Name: "foo", - }, - }, - }, - }}, - {chart.Chart{ - Metadata: &chart.Metadata{ - Name: "badchart", - Version: "0.1.0", - APIVersion: "v2", - Dependencies: []*chart.Dependency{ - { - Name: "foo", - Alias: "bar", - }, - { - Name: "bar", - }, - }, - }, - }}, - {chart.Chart{ - Metadata: &chart.Metadata{ - Name: "badchart", - Version: "0.1.0", - APIVersion: "v2", - Dependencies: []*chart.Dependency{ - { - Name: "foo", - Alias: "baz", - }, - { - Name: "bar", - Alias: "baz", - }, - }, - }, - }}, - } - - for _, tt := range tests { - if err := validateDependenciesUnique(&tt.chart); err == nil { - t.Errorf("chart should have been flagged for dependency shadowing") - } - } -} - -func TestDependencies(t *testing.T) { - tmp := t.TempDir() - - c := chartWithBadDependencies() - err := chartutil.SaveDir(&c, tmp) - if err != nil { - t.Fatal(err) - } - linter := support.Linter{ChartDir: filepath.Join(tmp, c.Metadata.Name)} - - Dependencies(&linter) - if l := len(linter.Messages); l != 2 { - t.Errorf("expected 2 linter errors for bad chart dependencies. Got %d.", l) - for i, msg := range linter.Messages { - t.Logf("Message: %d, Error: %#v", i, msg) - } - } -} diff --git a/pkg/helm/pkg/lint/rules/deprecations.go b/pkg/helm/pkg/lint/rules/deprecations.go deleted file mode 100644 index 162b2ba5..00000000 --- a/pkg/helm/pkg/lint/rules/deprecations.go +++ /dev/null @@ -1,106 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules // import "helm.sh/helm/v3/pkg/lint/rules" - -import ( - "fmt" - "strconv" - - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apiserver/pkg/endpoints/deprecation" - kscheme "k8s.io/client-go/kubernetes/scheme" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" -) - -var ( - // This should be set in the Makefile based on the version of client-go being imported. - // These constants will be overwritten with LDFLAGS. The version components must be - // strings in order for LDFLAGS to set them. - k8sVersionMajor = "1" - k8sVersionMinor = "20" -) - -// deprecatedAPIError indicates than an API is deprecated in Kubernetes -type deprecatedAPIError struct { - Deprecated string - Message string -} - -func (e deprecatedAPIError) Error() string { - msg := e.Message - return msg -} - -func validateNoDeprecations(resource *K8sYamlStruct, kubeVersion *chartutil.KubeVersion) error { - // if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation - if resource.APIVersion == "" { - return nil - } - if resource.Kind == "" { - return nil - } - - majorVersion := k8sVersionMajor - minorVersion := k8sVersionMinor - - if kubeVersion != nil { - majorVersion = kubeVersion.Major - minorVersion = kubeVersion.Minor - } - - runtimeObject, err := resourceToRuntimeObject(resource) - if err != nil { - // do not error for non-kubernetes resources - if runtime.IsNotRegisteredError(err) { - return nil - } - return err - } - - maj, err := strconv.Atoi(majorVersion) - if err != nil { - return err - } - min, err := strconv.Atoi(minorVersion) - if err != nil { - return err - } - - if !deprecation.IsDeprecated(runtimeObject, maj, min) { - return nil - } - gvk := fmt.Sprintf("%s %s", resource.APIVersion, resource.Kind) - return deprecatedAPIError{ - Deprecated: gvk, - Message: deprecation.WarningMessage(runtimeObject), - } -} - -func resourceToRuntimeObject(resource *K8sYamlStruct) (runtime.Object, error) { - scheme := runtime.NewScheme() - kscheme.AddToScheme(scheme) - - gvk := schema.FromAPIVersionAndKind(resource.APIVersion, resource.Kind) - out, err := scheme.New(gvk) - if err != nil { - return nil, err - } - out.GetObjectKind().SetGroupVersionKind(gvk) - return out, nil -} diff --git a/pkg/helm/pkg/lint/rules/deprecations_test.go b/pkg/helm/pkg/lint/rules/deprecations_test.go deleted file mode 100644 index cf240900..00000000 --- a/pkg/helm/pkg/lint/rules/deprecations_test.go +++ /dev/null @@ -1,41 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules // import "helm.sh/helm/v3/pkg/lint/rules" - -import "testing" - -func TestValidateNoDeprecations(t *testing.T) { - deprecated := &K8sYamlStruct{ - APIVersion: "extensions/v1beta1", - Kind: "Deployment", - } - err := validateNoDeprecations(deprecated, nil) - if err == nil { - t.Fatal("Expected deprecated extension to be flagged") - } - depErr := err.(deprecatedAPIError) - if depErr.Message == "" { - t.Fatalf("Expected error message to be non-blank: %v", err) - } - - if err := validateNoDeprecations(&K8sYamlStruct{ - APIVersion: "v1", - Kind: "Pod", - }, nil); err != nil { - t.Errorf("Expected a v1 Pod to not be deprecated") - } -} diff --git a/pkg/helm/pkg/lint/rules/template.go b/pkg/helm/pkg/lint/rules/template.go deleted file mode 100644 index 43de1886..00000000 --- a/pkg/helm/pkg/lint/rules/template.go +++ /dev/null @@ -1,352 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules - -import ( - "bufio" - "bytes" - "fmt" - "io" - "os" - "path" - "path/filepath" - "regexp" - "strings" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/api/validation" - apipath "k8s.io/apimachinery/pkg/api/validation/path" - "k8s.io/apimachinery/pkg/util/validation/field" - "k8s.io/apimachinery/pkg/util/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/engine" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -var ( - crdHookSearch = regexp.MustCompile(`"?helm\.sh/hook"?:\s+crd-install`) - releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`) -) - -// Templates lints the templates in the Linter. -func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool, opts helmopts.HelmOptions) { - TemplatesWithKubeVersion(linter, values, namespace, nil, opts) -} - -// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. -func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion, opts helmopts.HelmOptions) { - fpath := "templates/" - templatesPath := filepath.Join(linter.ChartDir, fpath) - - templatesDirExist := linter.RunLinterRule(support.WarningSev, fpath, validateTemplatesDir(templatesPath)) - - // Templates directory is optional for now - if !templatesDirExist { - return - } - - // Load chart and parse templates - chart, err := loader.Load(linter.ChartDir, opts) - - chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) - - if !chartLoaded { - return - } - - options := chartutil.ReleaseOptions{ - Name: "test-release", - Namespace: namespace, - } - - caps := chartutil.DefaultCapabilities.Copy() - if kubeVersion != nil { - caps.KubeVersion = *kubeVersion - } - - // lint ignores import-values - // See https://github.com/helm/helm/issues/9658 - if err := chartutil.ProcessDependenciesWithMerge(chart, &values); err != nil { - return - } - - cvals, err := chartutil.CoalesceValues(chart, values) - if err != nil { - return - } - - valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, caps, nil, nil) - if err != nil { - linter.RunLinterRule(support.ErrorSev, fpath, err) - return - } - var e engine.Engine - e.LintMode = true - renderedContentMap, err := e.Render(chart, valuesToRender, opts) - - renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) - - if !renderOk { - return - } - - /* Iterate over all the templates to check: - - It is a .yaml file - - All the values in the template file is defined - - {{}} include | quote - - Generated content is a valid Yaml file - - Metadata.Namespace is not set - */ - for _, template := range chart.Templates { - fileName, data := template.Name, template.Data - fpath = fileName - - linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) - // These are v3 specific checks to make sure and warn people if their - // chart is not compatible with v3 - linter.RunLinterRule(support.WarningSev, fpath, validateNoCRDHooks(data)) - linter.RunLinterRule(support.ErrorSev, fpath, validateNoReleaseTime(data)) - - // We only apply the following lint rules to yaml files - if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { - continue - } - - // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1463 - // Check that all the templates have a matching value - // linter.RunLinterRule(support.WarningSev, fpath, validateNoMissingValues(templatesPath, valuesToRender, preExecutedTemplate)) - - // NOTE: disabled for now, Refs https://github.com/helm/helm/issues/1037 - // linter.RunLinterRule(support.WarningSev, fpath, validateQuotes(string(preExecutedTemplate))) - - renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] - if strings.TrimSpace(renderedContent) != "" { - linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent)) - - decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) - - // Lint all resources if the file contains multiple documents separated by --- - for { - // Even though K8sYamlStruct only defines a few fields, an error in any other - // key will be raised as well - var yamlStruct *K8sYamlStruct - - err := decoder.Decode(&yamlStruct) - if err == io.EOF { - break - } - - // If YAML linting fails here, it will always fail in the next block as well, so we should return here. - // fix https://github.com/helm/helm/issues/11391 - if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { - return - } - if yamlStruct != nil { - // NOTE: set to warnings to allow users to support out-of-date kubernetes - // Refs https://github.com/helm/helm/issues/8596 - linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) - linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) - - linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) - linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) - } - } - } - } -} - -// validateTopIndentLevel checks that the content does not start with an indent level > 0. -// -// This error can occur when a template accidentally inserts space. It can cause -// unpredictable errors depending on whether the text is normalized before being passed -// into the YAML parser. So we trap it here. -// -// See https://github.com/helm/helm/issues/8467 -func validateTopIndentLevel(content string) error { - // Read lines until we get to a non-empty one - scanner := bufio.NewScanner(bytes.NewBufferString(content)) - for scanner.Scan() { - line := scanner.Text() - // If line is empty, skip - if strings.TrimSpace(line) == "" { - continue - } - // If it starts with one or more spaces, this is an error - if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { - return fmt.Errorf("document starts with an illegal indent: %q, which may cause parsing problems", line) - } - // Any other condition passes. - return nil - } - return scanner.Err() -} - -// Validation functions -func validateTemplatesDir(templatesPath string) error { - if fi, err := os.Stat(templatesPath); err == nil { - if !fi.IsDir() { - return errors.New("not a directory") - } - } - return nil -} - -func validateAllowedExtension(fileName string) error { - ext := filepath.Ext(fileName) - validExtensions := []string{".yaml", ".yml", ".tpl", ".txt"} - - for _, b := range validExtensions { - if b == ext { - return nil - } - } - - return errors.Errorf("file extension '%s' not valid. Valid extensions are .yaml, .yml, .tpl, or .txt", ext) -} - -func validateYamlContent(err error) error { - return errors.Wrap(err, "unable to parse YAML") -} - -// validateMetadataName uses the correct validation function for the object -// Kind, or if not set, defaults to the standard definition of a subdomain in -// DNS (RFC 1123), used by most resources. -func validateMetadataName(obj *K8sYamlStruct) error { - fn := validateMetadataNameFunc(obj) - allErrs := field.ErrorList{} - for _, msg := range fn(obj.Metadata.Name, false) { - allErrs = append(allErrs, field.Invalid(field.NewPath("metadata").Child("name"), obj.Metadata.Name, msg)) - } - if len(allErrs) > 0 { - return errors.Wrapf(allErrs.ToAggregate(), "object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name) - } - return nil -} - -// validateMetadataNameFunc will return a name validation function for the -// object kind, if defined below. -// -// Rules should match those set in the various api validations: -// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/core/validation/validation.go#L205-L274 -// https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/apps/validation/validation.go#L39 -// ... -// -// Implementing here to avoid importing k/k. -// -// If no mapping is defined, returns NameIsDNSSubdomain. This is used by object -// kinds that don't have special requirements, so is the most likely to work if -// new kinds are added. -func validateMetadataNameFunc(obj *K8sYamlStruct) validation.ValidateNameFunc { - switch strings.ToLower(obj.Kind) { - case "pod", "node", "secret", "endpoints", "resourcequota", // core - "controllerrevision", "daemonset", "deployment", "replicaset", "statefulset", // apps - "autoscaler", // autoscaler - "cronjob", "job", // batch - "lease", // coordination - "endpointslice", // discovery - "networkpolicy", "ingress", // networking - "podsecuritypolicy", // policy - "priorityclass", // scheduling - "podpreset", // settings - "storageclass", "volumeattachment", "csinode": // storage - return validation.NameIsDNSSubdomain - case "service": - return validation.NameIsDNS1035Label - case "namespace": - return validation.ValidateNamespaceName - case "serviceaccount": - return validation.ValidateServiceAccountName - case "certificatesigningrequest": - // No validation. - // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/certificates/validation/validation.go#L137-L140 - return func(name string, prefix bool) []string { return nil } - case "role", "clusterrole", "rolebinding", "clusterrolebinding": - // https://github.com/kubernetes/kubernetes/blob/v1.20.0/pkg/apis/rbac/validation/validation.go#L32-L34 - return func(name string, prefix bool) []string { - return apipath.IsValidPathSegmentName(name) - } - default: - return validation.NameIsDNSSubdomain - } -} - -func validateNoCRDHooks(manifest []byte) error { - if crdHookSearch.Match(manifest) { - return errors.New("manifest is a crd-install hook. This hook is no longer supported in v3 and all CRDs should also exist the crds/ directory at the top level of the chart") - } - return nil -} - -func validateNoReleaseTime(manifest []byte) error { - if releaseTimeSearch.Match(manifest) { - return errors.New(".Release.Time has been removed in v3, please replace with the `now` function in your templates") - } - return nil -} - -// validateMatchSelector ensures that template specs have a selector declared. -// See https://github.com/helm/helm/issues/1990 -func validateMatchSelector(yamlStruct *K8sYamlStruct, manifest string) error { - switch yamlStruct.Kind { - case "Deployment", "ReplicaSet", "DaemonSet", "StatefulSet": - // verify that matchLabels or matchExpressions is present - if !(strings.Contains(manifest, "matchLabels") || strings.Contains(manifest, "matchExpressions")) { - return fmt.Errorf("a %s must contain matchLabels or matchExpressions, and %q does not", yamlStruct.Kind, yamlStruct.Metadata.Name) - } - } - return nil -} -func validateListAnnotations(yamlStruct *K8sYamlStruct, manifest string) error { - if yamlStruct.Kind == "List" { - m := struct { - Items []struct { - Metadata struct { - Annotations map[string]string - } - } - }{} - - if err := yaml.Unmarshal([]byte(manifest), &m); err != nil { - return validateYamlContent(err) - } - - for _, i := range m.Items { - if _, ok := i.Metadata.Annotations["helm.sh/resource-policy"]; ok { - return errors.New("Annotation 'helm.sh/resource-policy' within List objects are ignored") - } - } - } - return nil -} - -// K8sYamlStruct stubs a Kubernetes YAML file. -// -// DEPRECATED: In Helm 4, this will be made a private type, as it is for use only within -// the rules package. -type K8sYamlStruct struct { - APIVersion string `json:"apiVersion"` - Kind string - Metadata k8sYamlMetadata -} - -type k8sYamlMetadata struct { - Namespace string - Name string -} diff --git a/pkg/helm/pkg/lint/rules/template_test.go b/pkg/helm/pkg/lint/rules/template_test.go deleted file mode 100644 index 9b67e8bd..00000000 --- a/pkg/helm/pkg/lint/rules/template_test.go +++ /dev/null @@ -1,460 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" -) - -const templateTestBasedir = "./testdata/albatross" - -func TestValidateAllowedExtension(t *testing.T) { - var failTest = []string{"/foo", "/test.toml"} - for _, test := range failTest { - err := validateAllowedExtension(test) - if err == nil || !strings.Contains(err.Error(), "Valid extensions are .yaml, .yml, .tpl, or .txt") { - t.Errorf("validateAllowedExtension('%s') to return \"Valid extensions are .yaml, .yml, .tpl, or .txt\", got no error", test) - } - } - var successTest = []string{"/foo.yaml", "foo.yaml", "foo.tpl", "/foo/bar/baz.yaml", "NOTES.txt"} - for _, test := range successTest { - err := validateAllowedExtension(test) - if err != nil { - t.Errorf("validateAllowedExtension('%s') to return no error but got \"%s\"", test, err.Error()) - } - } -} - -var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} - -const namespace = "testNamespace" -const strict = false - -func TestTemplateParsing(t *testing.T) { - linter := support.Linter{ChartDir: templateTestBasedir} - Templates(&linter, values, namespace, strict) - res := linter.Messages - - if len(res) != 1 { - t.Fatalf("Expected one error, got %d, %v", len(res), res) - } - - if !strings.Contains(res[0].Err.Error(), "deliberateSyntaxError") { - t.Errorf("Unexpected error: %s", res[0]) - } -} - -var wrongTemplatePath = filepath.Join(templateTestBasedir, "templates", "fail.yaml") -var ignoredTemplatePath = filepath.Join(templateTestBasedir, "fail.yaml.ignored") - -// Test a template with all the existing features: -// namespaces, partial templates -func TestTemplateIntegrationHappyPath(t *testing.T) { - // Rename file so it gets ignored by the linter - os.Rename(wrongTemplatePath, ignoredTemplatePath) - defer os.Rename(ignoredTemplatePath, wrongTemplatePath) - - linter := support.Linter{ChartDir: templateTestBasedir} - Templates(&linter, values, namespace, strict) - res := linter.Messages - - if len(res) != 0 { - t.Fatalf("Expected no error, got %d, %v", len(res), res) - } -} - -func TestV3Fail(t *testing.T) { - linter := support.Linter{ChartDir: "./testdata/v3-fail"} - Templates(&linter, values, namespace, strict) - res := linter.Messages - - if len(res) != 3 { - t.Fatalf("Expected 3 errors, got %d, %v", len(res), res) - } - - if !strings.Contains(res[0].Err.Error(), ".Release.Time has been removed in v3") { - t.Errorf("Unexpected error: %s", res[0].Err) - } - if !strings.Contains(res[1].Err.Error(), "manifest is a crd-install hook") { - t.Errorf("Unexpected error: %s", res[1].Err) - } - if !strings.Contains(res[2].Err.Error(), "manifest is a crd-install hook") { - t.Errorf("Unexpected error: %s", res[2].Err) - } -} - -func TestMultiTemplateFail(t *testing.T) { - linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} - Templates(&linter, values, namespace, strict) - res := linter.Messages - - if len(res) != 1 { - t.Fatalf("Expected 1 error, got %d, %v", len(res), res) - } - - if !strings.Contains(res[0].Err.Error(), "object name does not conform to Kubernetes naming requirements") { - t.Errorf("Unexpected error: %s", res[0].Err) - } -} - -func TestValidateMetadataName(t *testing.T) { - tests := []struct { - obj *K8sYamlStruct - wantErr bool - }{ - // Most kinds use IsDNS1123Subdomain. - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: ""}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, - {&K8sYamlStruct{Kind: "Pod", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, - {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, - {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, - {&K8sYamlStruct{Kind: "ServiceAccount", Metadata: k8sYamlMetadata{Name: "operator:sa"}}, true}, - - // Service uses IsDNS1035Label. - {&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "123baz"}}, true}, - {&K8sYamlStruct{Kind: "Service", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, - - // Namespace uses IsDNS1123Label. - {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, - {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, true}, - {&K8sYamlStruct{Kind: "Namespace", Metadata: k8sYamlMetadata{Name: "foo-bar"}}, false}, - - // CertificateSigningRequest has no validation. - {&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: ""}}, false}, - {&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, - {&K8sYamlStruct{Kind: "CertificateSigningRequest", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, false}, - - // RBAC uses path validation. - {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, - {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, - {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, - {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, - {&K8sYamlStruct{Kind: "Role", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, - {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, - {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "foo.bar"}}, false}, - {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, - {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator/role"}}, true}, - {&K8sYamlStruct{Kind: "ClusterRole", Metadata: k8sYamlMetadata{Name: "operator%role"}}, true}, - {&K8sYamlStruct{Kind: "RoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, - {&K8sYamlStruct{Kind: "ClusterRoleBinding", Metadata: k8sYamlMetadata{Name: "operator:role"}}, false}, - - // Unknown Kind - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: ""}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.bar1234baz.seventyone"}}, false}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "FOO"}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "123baz"}}, false}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "foo.BAR.baz"}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one-two"}}, false}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "-two"}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "one_two"}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "a..b"}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "%^&#$%*@^*@&#^"}}, true}, - {&K8sYamlStruct{Kind: "FutureKind", Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, - - // No kind - {&K8sYamlStruct{Metadata: k8sYamlMetadata{Name: "foo"}}, false}, - {&K8sYamlStruct{Metadata: k8sYamlMetadata{Name: "operator:pod"}}, true}, - } - for _, tt := range tests { - t.Run(fmt.Sprintf("%s/%s", tt.obj.Kind, tt.obj.Metadata.Name), func(t *testing.T) { - if err := validateMetadataName(tt.obj); (err != nil) != tt.wantErr { - t.Errorf("validateMetadataName() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func TestDeprecatedAPIFails(t *testing.T) { - mychart := chart.Chart{ - Metadata: &chart.Metadata{ - APIVersion: "v2", - Name: "failapi", - Version: "0.1.0", - Icon: "satisfy-the-linting-gods.gif", - }, - Templates: []*chart.File{ - { - Name: "templates/baddeployment.yaml", - Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), - }, - { - Name: "templates/goodsecret.yaml", - Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), - }, - }, - } - tmpdir := t.TempDir() - - if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { - t.Fatal(err) - } - - linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} - Templates(&linter, values, namespace, strict) - if l := len(linter.Messages); l != 1 { - for i, msg := range linter.Messages { - t.Logf("Message %d: %s", i, msg) - } - t.Fatalf("Expected 1 lint error, got %d", l) - } - - err := linter.Messages[0].Err.(deprecatedAPIError) - if err.Deprecated != "apps/v1beta1 Deployment" { - t.Errorf("Surprised to learn that %q is deprecated", err.Deprecated) - } -} - -const manifest = `apiVersion: v1 -kind: ConfigMap -metadata: - name: foo -data: - myval1: {{default "val" .Values.mymap.key1 }} - myval2: {{default "val" .Values.mymap.key2 }} -` - -// TestStrictTemplateParsingMapError is a regression test. -// -// The template engine should not produce an error when a map in values.yaml does -// not contain all possible keys. -// -// See https://github.com/helm/helm/issues/7483 -func TestStrictTemplateParsingMapError(t *testing.T) { - - ch := chart.Chart{ - Metadata: &chart.Metadata{ - Name: "regression7483", - APIVersion: "v2", - Version: "0.1.0", - }, - Values: map[string]interface{}{ - "mymap": map[string]string{ - "key1": "val1", - }, - }, - Templates: []*chart.File{ - { - Name: "templates/configmap.yaml", - Data: []byte(manifest), - }, - }, - } - dir := t.TempDir() - if err := chartutil.SaveDir(&ch, dir); err != nil { - t.Fatal(err) - } - linter := &support.Linter{ - ChartDir: filepath.Join(dir, ch.Metadata.Name), - } - Templates(linter, ch.Values, namespace, strict) - if len(linter.Messages) != 0 { - t.Errorf("expected zero messages, got %d", len(linter.Messages)) - for i, msg := range linter.Messages { - t.Logf("Message %d: %q", i, msg) - } - } -} - -func TestValidateMatchSelector(t *testing.T) { - md := &K8sYamlStruct{ - APIVersion: "apps/v1", - Kind: "Deployment", - Metadata: k8sYamlMetadata{ - Name: "mydeployment", - }, - } - manifest := ` - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 3 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - ` - if err := validateMatchSelector(md, manifest); err != nil { - t.Error(err) - } - manifest = ` - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 3 - selector: - matchExpressions: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - ` - if err := validateMatchSelector(md, manifest); err != nil { - t.Error(err) - } - manifest = ` - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 3 - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - ` - if err := validateMatchSelector(md, manifest); err == nil { - t.Error("expected Deployment with no selector to fail") - } -} - -func TestValidateTopIndentLevel(t *testing.T) { - for doc, shouldFail := range map[string]bool{ - // Should not fail - "\n\n\n\t\n \t\n": false, - "apiVersion:foo\n bar:baz": false, - "\n\n\napiVersion:foo\n\n\n": false, - // Should fail - " apiVersion:foo": true, - "\n\n apiVersion:foo\n\n": true, - } { - if err := validateTopIndentLevel(doc); (err == nil) == shouldFail { - t.Errorf("Expected %t for %q", shouldFail, doc) - } - } - -} - -// TestEmptyWithCommentsManifests checks the lint is not failing against empty manifests that contains only comments -// See https://github.com/helm/helm/issues/8621 -func TestEmptyWithCommentsManifests(t *testing.T) { - mychart := chart.Chart{ - Metadata: &chart.Metadata{ - APIVersion: "v2", - Name: "emptymanifests", - Version: "0.1.0", - Icon: "satisfy-the-linting-gods.gif", - }, - Templates: []*chart.File{ - { - Name: "templates/empty-with-comments.yaml", - Data: []byte("#@formatter:off\n"), - }, - }, - } - tmpdir := t.TempDir() - - if err := chartutil.SaveDir(&mychart, tmpdir); err != nil { - t.Fatal(err) - } - - linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} - Templates(&linter, values, namespace, strict) - if l := len(linter.Messages); l > 0 { - for i, msg := range linter.Messages { - t.Logf("Message %d: %s", i, msg) - } - t.Fatalf("Expected 0 lint errors, got %d", l) - } -} -func TestValidateListAnnotations(t *testing.T) { - md := &K8sYamlStruct{ - APIVersion: "v1", - Kind: "List", - Metadata: k8sYamlMetadata{ - Name: "list", - }, - } - manifest := ` -apiVersion: v1 -kind: List -items: - - apiVersion: v1 - kind: ConfigMap - metadata: - annotations: - helm.sh/resource-policy: keep -` - - if err := validateListAnnotations(md, manifest); err == nil { - t.Fatal("expected list with nested keep annotations to fail") - } - - manifest = ` -apiVersion: v1 -kind: List -metadata: - annotations: - helm.sh/resource-policy: keep -items: - - apiVersion: v1 - kind: ConfigMap -` - - if err := validateListAnnotations(md, manifest); err != nil { - t.Fatalf("List objects keep annotations should pass. got: %s", err) - } -} diff --git a/pkg/helm/pkg/lint/rules/values.go b/pkg/helm/pkg/lint/rules/values.go deleted file mode 100644 index 306d1ad0..00000000 --- a/pkg/helm/pkg/lint/rules/values.go +++ /dev/null @@ -1,86 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules - -import ( - "os" - "path/filepath" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/lint/support" -) - -// Values lints a chart's values.yaml file. -// -// This function is deprecated and will be removed in Helm 4. -func Values(linter *support.Linter) { - ValuesWithOverrides(linter, map[string]interface{}{}) -} - -// ValuesWithOverrides tests the values.yaml file. -// -// If a schema is present in the chart, values are tested against that. Otherwise, -// they are only tested for well-formedness. -// -// If additional values are supplied, they are coalesced into the values in values.yaml. -func ValuesWithOverrides(linter *support.Linter, values map[string]interface{}) { - file := "values.yaml" - vf := filepath.Join(linter.ChartDir, file) - fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) - - if !fileExists { - return - } - - linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, values)) -} - -func validateValuesFileExistence(valuesPath string) error { - _, err := os.Stat(valuesPath) - if err != nil { - return errors.Errorf("file does not exist") - } - return nil -} - -func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { - values, err := chartutil.ReadValuesFile(valuesPath) - if err != nil { - return errors.Wrap(err, "unable to parse YAML") - } - - // Helm 3.0.0 carried over the values linting from Helm 2.x, which only tests the top - // level values against the top-level expectations. Subchart values are not linted. - // We could change that. For now, though, we retain that strategy, and thus can - // coalesce tables (like reuse-values does) instead of doing the full chart - // CoalesceValues - coalescedValues := chartutil.CoalesceTables(make(map[string]interface{}, len(overrides)), overrides) - coalescedValues = chartutil.CoalesceTables(coalescedValues, values) - - ext := filepath.Ext(valuesPath) - schemaPath := valuesPath[:len(valuesPath)-len(ext)] + ".schema.json" - schema, err := os.ReadFile(schemaPath) - if len(schema) == 0 { - return nil - } - if err != nil { - return err - } - return chartutil.ValidateAgainstSingleSchema(coalescedValues, schema) -} diff --git a/pkg/helm/pkg/lint/rules/values_test.go b/pkg/helm/pkg/lint/rules/values_test.go deleted file mode 100644 index f9a78636..00000000 --- a/pkg/helm/pkg/lint/rules/values_test.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package rules - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/werf/nelm/pkg/helm/intern/test/ensure" -) - -var nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") - -const testSchema = ` -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "helm values test schema", - "type": "object", - "additionalProperties": false, - "required": [ - "username", - "password" - ], - "properties": { - "username": { - "description": "Your username", - "type": "string" - }, - "password": { - "description": "Your password", - "type": "string" - } - } -} -` - -func TestValidateValuesYamlNotDirectory(t *testing.T) { - _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) - defer os.Remove(nonExistingValuesFilePath) - - err := validateValuesFileExistence(nonExistingValuesFilePath) - if err == nil { - t.Errorf("validateValuesFileExistence to return a linter error, got no error") - } -} - -func TestValidateValuesFileWellFormed(t *testing.T) { - badYaml := ` - not:well[]{}formed - ` - tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil { - t.Fatal("expected values file to fail parsing") - } -} - -func TestValidateValuesFileSchema(t *testing.T) { - yaml := "username: admin\npassword: swordfish" - tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) - createTestingSchema(t, tmpdir) - - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil { - t.Fatalf("Failed validation with %s", err) - } -} - -func TestValidateValuesFileSchemaFailure(t *testing.T) { - // 1234 is an int, not a string. This should fail. - yaml := "username: 1234\npassword: swordfish" - tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) - createTestingSchema(t, tmpdir) - - valfile := filepath.Join(tmpdir, "values.yaml") - - err := validateValuesFile(valfile, map[string]interface{}{}) - if err == nil { - t.Fatal("expected values file to fail parsing") - } - - assert.Contains(t, err.Error(), "Expected: string, given: integer", "integer should be caught by schema") -} - -func TestValidateValuesFileSchemaOverrides(t *testing.T) { - yaml := "username: admin" - overrides := map[string]interface{}{ - "password": "swordfish", - } - tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) - createTestingSchema(t, tmpdir) - - valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, overrides); err != nil { - t.Fatalf("Failed validation with %s", err) - } -} - -func TestValidateValuesFile(t *testing.T) { - tests := []struct { - name string - yaml string - overrides map[string]interface{} - errorMessage string - }{ - { - name: "value added", - yaml: "username: admin", - overrides: map[string]interface{}{"password": "swordfish"}, - }, - { - name: "value not overridden", - yaml: "username: admin\npassword:", - overrides: map[string]interface{}{"username": "anotherUser"}, - errorMessage: "Expected: string, given: null", - }, - { - name: "value overridden", - yaml: "username: admin\npassword:", - overrides: map[string]interface{}{"username": "anotherUser", "password": "swordfish"}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - tmpdir := ensure.TempFile(t, "values.yaml", []byte(tt.yaml)) - createTestingSchema(t, tmpdir) - - valfile := filepath.Join(tmpdir, "values.yaml") - - err := validateValuesFile(valfile, tt.overrides) - - switch { - case err != nil && tt.errorMessage == "": - t.Errorf("Failed validation with %s", err) - case err == nil && tt.errorMessage != "": - t.Error("expected values file to fail parsing") - case err != nil && tt.errorMessage != "": - assert.Contains(t, err.Error(), tt.errorMessage, "Failed with unexpected error") - } - }) - } -} - -func createTestingSchema(t *testing.T, dir string) string { - t.Helper() - schemafile := filepath.Join(dir, "values.schema.json") - if err := os.WriteFile(schemafile, []byte(testSchema), 0700); err != nil { - t.Fatalf("Failed to write schema to tmpdir: %s", err) - } - return schemafile -} diff --git a/pkg/helm/pkg/lint/support/doc.go b/pkg/helm/pkg/lint/support/doc.go deleted file mode 100644 index bffefe8f..00000000 --- a/pkg/helm/pkg/lint/support/doc.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -/* -Package support contains tools for linting charts. - -Linting is the process of testing charts for errors or warnings regarding -formatting, compilation, or standards compliance. -*/ -package support // import "helm.sh/helm/v3/pkg/lint/support" diff --git a/pkg/helm/pkg/lint/support/message.go b/pkg/helm/pkg/lint/support/message.go deleted file mode 100644 index f978f7d4..00000000 --- a/pkg/helm/pkg/lint/support/message.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package support - -import ( - "fmt" - - "github.com/werf/nelm/pkg/helm/pkg/errs" -) - -// Severity indicates the severity of a Message. -const ( - // UnknownSev indicates that the severity of the error is unknown, and should not stop processing. - UnknownSev = iota - // InfoSev indicates information, for example missing values.yaml file - InfoSev - // WarningSev indicates that something does not meet code standards, but will likely function. - WarningSev - // ErrorSev indicates that something will not likely function. - ErrorSev -) - -// sev matches the *Sev states. -var sev = []string{"UNKNOWN", "INFO", "WARNING", "ERROR"} - -// Linter encapsulates a linting run of a particular chart. -type Linter struct { - Messages []Message - // The highest severity of all the failing lint rules - HighestSeverity int - ChartDir string -} - -// Message describes an error encountered while linting. -type Message struct { - // Severity is one of the *Sev constants - Severity int - Path string - Err error -} - -func (m Message) Error() string { - return fmt.Sprintf("[%s] %s: %s", sev[m.Severity], m.Path, m.Err.Error()) -} - -// NewMessage creates a new Message struct -func NewMessage(severity int, path string, err error) Message { - return Message{Severity: severity, Path: path, Err: err} -} - -// RunLinterRule returns true if the validation passed -func (l *Linter) RunLinterRule(severity int, path string, err error) bool { - // severity is out of bound - if severity < 0 || severity >= len(sev) { - return false - } - - if err != nil { - l.Messages = append(l.Messages, NewMessage(severity, path, errs.FormatTemplatingError(err))) - - if severity > l.HighestSeverity { - l.HighestSeverity = severity - } - } - return err == nil -} diff --git a/pkg/helm/pkg/lint/support/message_test.go b/pkg/helm/pkg/lint/support/message_test.go deleted file mode 100644 index 9e12a638..00000000 --- a/pkg/helm/pkg/lint/support/message_test.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package support - -import ( - "testing" - - "github.com/pkg/errors" -) - -var linter = Linter{} -var errLint = errors.New("lint failed") - -func TestRunLinterRule(t *testing.T) { - var tests = []struct { - Severity int - LintError error - ExpectedMessages int - ExpectedReturn bool - ExpectedHighestSeverity int - }{ - {InfoSev, errLint, 1, false, InfoSev}, - {WarningSev, errLint, 2, false, WarningSev}, - {ErrorSev, errLint, 3, false, ErrorSev}, - // No error so it returns true - {ErrorSev, nil, 3, true, ErrorSev}, - // Retains highest severity - {InfoSev, errLint, 4, false, ErrorSev}, - // Invalid severity values - {4, errLint, 4, false, ErrorSev}, - {22, errLint, 4, false, ErrorSev}, - {-1, errLint, 4, false, ErrorSev}, - } - - for _, test := range tests { - isValid := linter.RunLinterRule(test.Severity, "chart", test.LintError) - if len(linter.Messages) != test.ExpectedMessages { - t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.Messages should now have %d message, we got %d", test.Severity, test.LintError, test.ExpectedMessages, len(linter.Messages)) - } - - if linter.HighestSeverity != test.ExpectedHighestSeverity { - t.Errorf("RunLinterRule(%d, \"chart\", %v), linter.HighestSeverity should be %d, we got %d", test.Severity, test.LintError, test.ExpectedHighestSeverity, linter.HighestSeverity) - } - - if isValid != test.ExpectedReturn { - t.Errorf("RunLinterRule(%d, \"chart\", %v), should have returned %t but returned %t", test.Severity, test.LintError, test.ExpectedReturn, isValid) - } - } -} - -func TestMessage(t *testing.T) { - m := Message{ErrorSev, "Chart.yaml", errors.New("Foo")} - if m.Error() != "[ERROR] Chart.yaml: Foo" { - t.Errorf("Unexpected output: %s", m.Error()) - } - - m = Message{WarningSev, "templates/", errors.New("Bar")} - if m.Error() != "[WARNING] templates/: Bar" { - t.Errorf("Unexpected output: %s", m.Error()) - } - - m = Message{InfoSev, "templates/rc.yaml", errors.New("FooBar")} - if m.Error() != "[INFO] templates/rc.yaml: FooBar" { - t.Errorf("Unexpected output: %s", m.Error()) - } -} diff --git a/pkg/helm/pkg/phases/deployed_resources_calculator.go b/pkg/helm/pkg/phases/deployed_resources_calculator.go deleted file mode 100644 index 4f568498..00000000 --- a/pkg/helm/pkg/phases/deployed_resources_calculator.go +++ /dev/null @@ -1,104 +0,0 @@ -package phases - -import ( - "fmt" - "math" - - "github.com/werf/nelm/pkg/helm/pkg/kube" - rel "github.com/werf/nelm/pkg/helm/pkg/release" -) - -func NewDeployedResourcesCalculator(history []*rel.Release, stagesSplitter Splitter, kubeClient kube.Interface) *DeployedResourcesCalculator { - return &DeployedResourcesCalculator{ - history: history, - stagesSplitter: stagesSplitter, - kubeClient: kubeClient, - } -} - -type DeployedResourcesCalculator struct { - history []*rel.Release - stagesSplitter Splitter - kubeClient kube.Interface -} - -func (c *DeployedResourcesCalculator) Calculate() (kube.ResourceList, error) { - lastDeployedReleaseIndex := c.lastDeployedReleaseIndex() - lastUninstalledReleaseIndex := c.lastUninstalledReleaseIndex() - - startAt := c.calculateRevisionToStartAt(lastDeployedReleaseIndex, lastUninstalledReleaseIndex) - if startAt == nil { - return nil, nil - } - - result := kube.ResourceList{} - for i := *startAt; i < len(c.history); i++ { - release := c.history[i] - - switch release.Info.Status { - case - rel.StatusDeployed, - rel.StatusSuperseded, - rel.StatusFailed, - rel.StatusPendingInstall, - rel.StatusPendingUpgrade, - rel.StatusPendingRollback, - rel.StatusUninstalling: - mainPhase, err := NewRolloutPhase(release, c.stagesSplitter, c.kubeClient). - ParseStagesFromString(release.Manifest) - if err != nil { - return nil, fmt.Errorf("error creating main phase: %w", err) - } - - result.Merge(mainPhase.DeployedResources()) - case rel.StatusUninstalled, rel.StatusUnknown: - default: - panic(fmt.Sprintf("unexpected release status: %s", release.Info.Status)) - } - } - - return result, nil -} - -func (c *DeployedResourcesCalculator) calculateRevisionToStartAt(lastDeployedReleaseIndex, lastUninstalledReleaseIndex *int) *int { - if lastDeployedReleaseIndex == nil && lastUninstalledReleaseIndex == nil { - firstRev := 0 - return &firstRev - } else if lastDeployedReleaseIndex != nil && lastUninstalledReleaseIndex == nil { - return lastDeployedReleaseIndex - } else if lastDeployedReleaseIndex == nil && lastUninstalledReleaseIndex != nil { - if *lastUninstalledReleaseIndex == (len(c.history) - 1) { - return nil - } - - result := *lastUninstalledReleaseIndex + 1 - return &result - } else { - if *lastUninstalledReleaseIndex == (len(c.history) - 1) { - return lastDeployedReleaseIndex - } - - result := int(math.Max(float64(*lastDeployedReleaseIndex), float64(*lastUninstalledReleaseIndex+1))) - return &result - } -} - -func (c *DeployedResourcesCalculator) lastDeployedReleaseIndex() *int { - for i := len(c.history) - 1; i >= 0; i-- { - if c.history[i].Info.Status == rel.StatusDeployed || c.history[i].Info.Status == rel.StatusSuperseded { - return &i - } - } - - return nil -} - -func (c *DeployedResourcesCalculator) lastUninstalledReleaseIndex() *int { - for i := len(c.history) - 1; i >= 0; i-- { - if c.history[i].Info.Status == rel.StatusUninstalled { - return &i - } - } - - return nil -} diff --git a/pkg/helm/pkg/phases/interfaces.go b/pkg/helm/pkg/phases/interfaces.go deleted file mode 100644 index 3625b04a..00000000 --- a/pkg/helm/pkg/phases/interfaces.go +++ /dev/null @@ -1,14 +0,0 @@ -package phases - -import ( - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" -) - -type Splitter interface { - Split(resources kube.ResourceList) (stages.SortedStageList, error) -} - -type ExternalDepsGenerator interface { - Generate(stages stages.SortedStageList) error -} diff --git a/pkg/helm/pkg/phases/no_external_deps_generator.go b/pkg/helm/pkg/phases/no_external_deps_generator.go deleted file mode 100644 index 03052a87..00000000 --- a/pkg/helm/pkg/phases/no_external_deps_generator.go +++ /dev/null @@ -1,11 +0,0 @@ -package phases - -import ( - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" -) - -type NoExternalDepsGenerator struct{} - -func (g *NoExternalDepsGenerator) Generate(_ stages.SortedStageList) error { - return nil -} diff --git a/pkg/helm/pkg/phases/phasemanagers/rollout_phase_manager.go b/pkg/helm/pkg/phases/phasemanagers/rollout_phase_manager.go deleted file mode 100644 index d6c3b058..00000000 --- a/pkg/helm/pkg/phases/phasemanagers/rollout_phase_manager.go +++ /dev/null @@ -1,113 +0,0 @@ -package phasemanagers - -import ( - "fmt" - "strings" - - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" - rel "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/storage" -) - -func NewRolloutPhaseManager(rolloutPhase *phases.RolloutPhase, deployedResCalc *phases.DeployedResourcesCalculator, release *rel.Release, storage *storage.Storage, kubeClient kube.Interface) *RolloutPhaseManager { - return &RolloutPhaseManager{ - Phase: rolloutPhase, - Release: release, - Storage: storage, - deployedResourcesCalculator: deployedResCalc, - kubeClient: kubeClient, - } -} - -type RolloutPhaseManager struct { - Phase *phases.RolloutPhase - Release *rel.Release - Storage *storage.Storage - - deployedResourcesCalculator *phases.DeployedResourcesCalculator - previouslyDeployedResources kube.ResourceList - kubeClient kube.Interface -} - -func (m *RolloutPhaseManager) AddCalculatedPreviouslyDeployedResources() (*RolloutPhaseManager, error) { - resources, err := m.deployedResourcesCalculator.Calculate() - if err != nil { - return nil, fmt.Errorf("error calculating previously deployed resources: %w", err) - } - - m.previouslyDeployedResources.Merge(resources) - - return m, nil -} - -func (m *RolloutPhaseManager) AddPreviouslyDeployedResources(resources kube.ResourceList) *RolloutPhaseManager { - m.previouslyDeployedResources.Merge(resources) - - return m -} - -func (m *RolloutPhaseManager) DoStage( - extDepTrackFn func(stgIndex int, stage *stages.Stage) error, - applyFn func(stgIndex int, stage *stages.Stage, prevDeployedStgResources kube.ResourceList) error, - trackFn func(stgIndex int, stage *stages.Stage) error, -) error { - for i, stg := range m.Phase.SortedStages { - if err := extDepTrackFn(i, stg); err != nil { - return fmt.Errorf("error tracking external dependencies: %w", err) - } - - if err := applyFn(i, stg, m.previouslyDeployedResources.Intersect(stg.DesiredResources)); err != nil { - return &ApplyError{StageIndex: i, Err: err} - } - - rel.SetRolloutPhaseStageInfo(m.Release, i) - if err := m.Storage.Update(m.Release); err != nil { - return fmt.Errorf("error updating release in storage: %w", err) - } - - if err := trackFn(i, stg); err != nil { - return fmt.Errorf("error tracking resources: %w", err) - } - } - - return nil -} - -func (m *RolloutPhaseManager) DeleteOrphanedResources() error { - orphanedResources := m.previouslyDeployedResources.Difference(m.Phase.AllResources()) - _, errs := m.kubeClient.Delete(orphanedResources, kube.DeleteOptions{ - Wait: true, - SkipIfInvalidOwnership: true, - ReleaseName: m.Release.Name, - ReleaseNamespace: m.Release.Namespace, - }) - if len(errs) > 0 { - return fmt.Errorf("while deleting previously deployed but now orphaned resources got %d error(s): %s", len(errs), joinErrors(errs)) - } - - return nil -} - -func joinErrors(errs []error) string { - es := make([]string, 0, len(errs)) - for _, e := range errs { - es = append(es, e.Error()) - } - - return strings.Join(es, "; ") -} - -type ApplyError struct { - StageIndex int - Err error -} - -func (e ApplyError) Error() string { - return fmt.Sprintf("error applying resources: %s", e.Err.Error()) -} - -func (e ApplyError) Unwrap() error { - return e.Err -} diff --git a/pkg/helm/pkg/phases/rollout_phase.go b/pkg/helm/pkg/phases/rollout_phase.go deleted file mode 100644 index e228a297..00000000 --- a/pkg/helm/pkg/phases/rollout_phase.go +++ /dev/null @@ -1,137 +0,0 @@ -package phases - -import ( - "bytes" - "fmt" - - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" - rel "github.com/werf/nelm/pkg/helm/pkg/release" -) - -func NewRolloutPhase(release *rel.Release, stagesSplitter Splitter, kubeClient kube.Interface) *RolloutPhase { - return &RolloutPhase{ - Release: release, - stagesSplitter: stagesSplitter, - kubeClient: kubeClient, - } -} - -type RolloutPhase struct { - SortedStages stages.SortedStageList - Release *rel.Release - - stagesSplitter Splitter - kubeClient kube.Interface -} - -func (m *RolloutPhase) ParseStagesFromString(manifests string) (*RolloutPhase, error) { - resources, err := m.kubeClient.Build(bytes.NewBufferString(manifests), false) - if err != nil { - return nil, fmt.Errorf("error building kubernetes objects from manifests: %w", err) - } - - return m.ParseStages(resources) -} - -func (m *RolloutPhase) ParseStages(resources kube.ResourceList) (*RolloutPhase, error) { - var err error - m.SortedStages, err = m.stagesSplitter.Split(resources) - if err != nil { - return nil, fmt.Errorf("error splitting rollout stage resources list: %w", err) - } - - return m, nil -} - -func (m *RolloutPhase) GenerateStagesExternalDeps(stagesExternalDepsGenerator ExternalDepsGenerator) error { - if err := stagesExternalDepsGenerator.Generate(m.SortedStages); err != nil { - return fmt.Errorf("error generating external deps for stages: %w", err) - } - - if err := m.validateStagesExternalDeps(); err != nil { - return fmt.Errorf("error validating external deps: %w", err) - } - - return nil -} - -func (m *RolloutPhase) DeployedResources() kube.ResourceList { - lastDeployedStageIndex := m.LastDeployedStageIndex() - if lastDeployedStageIndex == nil { - return nil - } - - return m.SortedStages.MergedDesiredResourcesInStagesRange(0, *lastDeployedStageIndex) -} - -func (m *RolloutPhase) AllResources() kube.ResourceList { - return m.SortedStages.MergedDesiredResources() -} - -func (m *RolloutPhase) LastDeployedStageIndex() *int { - if !m.IsPhaseStarted() { - return nil - } - - lastStage := len(m.SortedStages) - 1 - - if m.IsPhaseCompleted() { - return &lastStage - } - - // Phase started but not completed. - if m.Release.Info.LastStage == nil { - return &lastStage - } else { - return m.Release.Info.LastStage - } -} - -func (m *RolloutPhase) IsPhaseStarted() bool { - if m.Release.Info.LastPhase == nil { - return true - } - - switch *m.Release.Info.LastPhase { - case rel.PhaseRollout, rel.PhaseUninstall, rel.PhaseHooksPost, rel.PhaseHooksPre: - return true - default: - return false - } -} - -func (m *RolloutPhase) IsPhaseCompleted() bool { - if m.Release.Info.LastPhase == nil { - return true - } - - switch *m.Release.Info.LastPhase { - case rel.PhaseRollout: - if m.Release.Info.LastStage == nil { - return true - } else { - return *m.Release.Info.LastStage == len(m.SortedStages)-1 - } - case rel.PhaseHooksPost: - return true - default: - return false - } -} - -func (m *RolloutPhase) validateStagesExternalDeps() error { - phaseDesiredResources := m.SortedStages.MergedDesiredResources() - - for _, stage := range m.SortedStages { - for _, stageExtDep := range stage.ExternalDependencies { - for _, phaseDesiredRes := range phaseDesiredResources { - if kube.ResourceNameNamespaceKind(stageExtDep.Info) == kube.ResourceNameNamespaceKind(phaseDesiredRes) { - return fmt.Errorf("resources from current release can't be external dependencies: remove external dependency on %q", kube.ResourceNameNamespaceKind(stageExtDep.Info)) - } - } - } - } - - return nil -} diff --git a/pkg/helm/pkg/phases/single_stage_splitter.go b/pkg/helm/pkg/phases/single_stage_splitter.go deleted file mode 100644 index 65af0951..00000000 --- a/pkg/helm/pkg/phases/single_stage_splitter.go +++ /dev/null @@ -1,29 +0,0 @@ -package phases - -import ( - "fmt" - - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" - "k8s.io/cli-runtime/pkg/resource" -) - -type SingleStageSplitter struct{} - -func (s *SingleStageSplitter) Split(resources kube.ResourceList) (stages.SortedStageList, error) { - stage := &stages.Stage{} - - if err := resources.Visit(func(res *resource.Info, err error) error { - if err != nil { - return err - } - - stage.DesiredResources.Append(res) - - return nil - }); err != nil { - return nil, fmt.Errorf("error visiting resources list: %w", err) - } - - return stages.SortedStageList{stage}, nil -} diff --git a/pkg/helm/pkg/phases/stages/externaldeps/external_dependency.go b/pkg/helm/pkg/phases/stages/externaldeps/external_dependency.go deleted file mode 100644 index a479b5d3..00000000 --- a/pkg/helm/pkg/phases/stages/externaldeps/external_dependency.go +++ /dev/null @@ -1,55 +0,0 @@ -package externaldeps - -import ( - "fmt" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/cli-runtime/pkg/resource" -) - -func NewExternalDependency(name, resourceType, resourceName string) *ExternalDependency { - return &ExternalDependency{ - Name: name, - ResourceType: resourceType, - ResourceName: resourceName, - } -} - -type ExternalDependency struct { - Name string - ResourceType string - ResourceName string - - Namespace string - Info *resource.Info -} - -func (d *ExternalDependency) GenerateInfo(gvkBuilder GVKBuilder, metaAccessor meta.MetadataAccessor, mapper meta.RESTMapper) error { - gvk, err := gvkBuilder.BuildFromResource(d.ResourceType) - if err != nil { - return fmt.Errorf("error building GroupVersionKind from resource type: %w", err) - } - - mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version) - if err != nil { - return fmt.Errorf("error getting resource mapping: %w", err) - } - - object := unstructured.Unstructured{} - object.SetGroupVersionKind(*gvk) - object.SetName(d.ResourceName) - - d.Info = &resource.Info{ - Mapping: mapping, - Object: &object, - Name: d.ResourceName, - } - - if d.Info.Namespaced() { - d.Info.Namespace = d.Namespace - d.Info.Object.(*unstructured.Unstructured).SetNamespace(d.Namespace) - } - - return nil -} diff --git a/pkg/helm/pkg/phases/stages/externaldeps/external_dependency_list.go b/pkg/helm/pkg/phases/stages/externaldeps/external_dependency_list.go deleted file mode 100644 index 4ebcf2e8..00000000 --- a/pkg/helm/pkg/phases/stages/externaldeps/external_dependency_list.go +++ /dev/null @@ -1,16 +0,0 @@ -package externaldeps - -import ( - "github.com/werf/nelm/pkg/helm/pkg/kube" -) - -type ExternalDependencyList []*ExternalDependency - -func (l ExternalDependencyList) AsResourceList() kube.ResourceList { - resourceList := kube.ResourceList{} - for _, extDep := range l { - resourceList = append(resourceList, extDep.Info) - } - - return resourceList -} diff --git a/pkg/helm/pkg/phases/stages/externaldeps/interfaces.go b/pkg/helm/pkg/phases/stages/externaldeps/interfaces.go deleted file mode 100644 index b8d3b15a..00000000 --- a/pkg/helm/pkg/phases/stages/externaldeps/interfaces.go +++ /dev/null @@ -1,9 +0,0 @@ -package externaldeps - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" -) - -type GVKBuilder interface { - BuildFromResource(resource string) (*schema.GroupVersionKind, error) -} diff --git a/pkg/helm/pkg/phases/stages/sorted_stage_list.go b/pkg/helm/pkg/phases/stages/sorted_stage_list.go deleted file mode 100644 index ba385dc1..00000000 --- a/pkg/helm/pkg/phases/stages/sorted_stage_list.go +++ /dev/null @@ -1,100 +0,0 @@ -package stages - -import ( - "github.com/werf/nelm/pkg/helm/pkg/kube" -) - -type SortedStageList []*Stage - -func (l SortedStageList) Len() int { - return len(l) -} - -func (l SortedStageList) Less(i, j int) bool { - return l[i].Weight < l[j].Weight -} - -func (l SortedStageList) Swap(i, j int) { - l[i], l[j] = l[j], l[i] -} - -func (l SortedStageList) StageByWeight(weight int) *Stage { - for _, stg := range l { - if stg.Weight == weight { - return stg - } - } - - return nil -} - -func (l SortedStageList) MergedCreatedResources() kube.ResourceList { - return l.MergedCreatedResourcesInStagesRange(0, len(l)-1) -} - -func (l SortedStageList) MergedCreatedResourcesInStagesRange(first, last int) kube.ResourceList { - created := kube.ResourceList{} - for i := first; i <= last; i++ { - stg := l[i] - - if stg.Result == nil { - continue - } - - created.Merge(stg.Result.Created) - } - - return created -} - -func (l SortedStageList) MergedUpdatedResources() kube.ResourceList { - return l.MergedUpdatedResourcesInStagesRange(0, len(l)-1) -} - -func (l SortedStageList) MergedUpdatedResourcesInStagesRange(first, last int) kube.ResourceList { - updated := kube.ResourceList{} - for i := first; i <= last; i++ { - stg := l[i] - - if stg.Result == nil { - continue - } - - updated.Merge(stg.Result.Updated) - } - - return updated -} - -func (l SortedStageList) MergedDeletedResources() kube.ResourceList { - return l.MergedDeletedResourcesInStagesRange(0, len(l)-1) -} - -func (l SortedStageList) MergedDeletedResourcesInStagesRange(first, last int) kube.ResourceList { - deleted := kube.ResourceList{} - for i := first; i <= last; i++ { - stg := l[i] - - if stg.Result == nil { - continue - } - - deleted.Merge(stg.Result.Deleted) - } - - return deleted -} - -func (l SortedStageList) MergedDesiredResources() kube.ResourceList { - return l.MergedDesiredResourcesInStagesRange(0, len(l)-1) -} - -func (l SortedStageList) MergedDesiredResourcesInStagesRange(first, last int) kube.ResourceList { - resources := kube.ResourceList{} - for i := first; i <= last; i++ { - stg := l[i] - resources.Merge(stg.DesiredResources) - } - - return resources -} diff --git a/pkg/helm/pkg/phases/stages/stage.go b/pkg/helm/pkg/phases/stages/stage.go deleted file mode 100644 index 9d901110..00000000 --- a/pkg/helm/pkg/phases/stages/stage.go +++ /dev/null @@ -1,13 +0,0 @@ -package stages - -import ( - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages/externaldeps" -) - -type Stage struct { - Weight int - ExternalDependencies externaldeps.ExternalDependencyList - DesiredResources kube.ResourceList - Result *kube.Result -} diff --git a/pkg/helm/pkg/plugin/cache/cache.go b/pkg/helm/pkg/plugin/cache/cache.go deleted file mode 100644 index 5f3345b6..00000000 --- a/pkg/helm/pkg/plugin/cache/cache.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package cache provides a key generator for vcs urls. -package cache // import "helm.sh/helm/v3/pkg/plugin/cache" - -import ( - "net/url" - "regexp" - "strings" -) - -// Thanks glide! - -// scpSyntaxRe matches the SCP-like addresses used to access repos over SSH. -var scpSyntaxRe = regexp.MustCompile(`^([a-zA-Z0-9_]+)@([a-zA-Z0-9._-]+):(.*)$`) - -// Key generates a cache key based on a url or scp string. The key is file -// system safe. -func Key(repo string) (string, error) { - var ( - u *url.URL - err error - ) - if m := scpSyntaxRe.FindStringSubmatch(repo); m != nil { - // Match SCP-like syntax and convert it to a URL. - // Eg, "git@github.com:user/repo" becomes - // "ssh://git@github.com/user/repo". - u = &url.URL{ - User: url.User(m[1]), - Host: m[2], - Path: "/" + m[3], - } - } else { - u, err = url.Parse(repo) - if err != nil { - return "", err - } - } - - var key strings.Builder - if u.Scheme != "" { - key.WriteString(u.Scheme) - key.WriteString("-") - } - if u.User != nil && u.User.Username() != "" { - key.WriteString(u.User.Username()) - key.WriteString("-") - } - key.WriteString(u.Host) - if u.Path != "" { - key.WriteString(strings.ReplaceAll(u.Path, "/", "-")) - } - return strings.ReplaceAll(key.String(), ":", "-"), nil -} diff --git a/pkg/helm/pkg/plugin/hooks.go b/pkg/helm/pkg/plugin/hooks.go deleted file mode 100644 index e3481515..00000000 --- a/pkg/helm/pkg/plugin/hooks.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package plugin // import "helm.sh/helm/v3/pkg/plugin" - -// Types of hooks -const ( - // Install is executed after the plugin is added. - Install = "install" - // Delete is executed after the plugin is removed. - Delete = "delete" - // Update is executed after the plugin is updated. - Update = "update" -) - -// Hooks is a map of events to commands. -type Hooks map[string]string diff --git a/pkg/helm/pkg/plugin/installer/base.go b/pkg/helm/pkg/plugin/installer/base.go deleted file mode 100644 index 53215899..00000000 --- a/pkg/helm/pkg/plugin/installer/base.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "path/filepath" - - "github.com/werf/nelm/pkg/helm/pkg/cli" -) - -type base struct { - // Source is the reference to a plugin - Source string - // PluginsDirectory is the directory where plugins are installed - PluginsDirectory string -} - -func newBase(source string) base { - settings := cli.New() - return base{ - Source: source, - PluginsDirectory: settings.PluginsDirectory, - } -} - -// Path is where the plugin will be installed. -func (b *base) Path() string { - if b.Source == "" { - return "" - } - return filepath.Join(b.PluginsDirectory, filepath.Base(b.Source)) -} diff --git a/pkg/helm/pkg/plugin/installer/base_test.go b/pkg/helm/pkg/plugin/installer/base_test.go deleted file mode 100644 index 38ef28c3..00000000 --- a/pkg/helm/pkg/plugin/installer/base_test.go +++ /dev/null @@ -1,48 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "os" - "testing" -) - -func TestPath(t *testing.T) { - tests := []struct { - source string - helmPluginsDir string - expectPath string - }{ - { - source: "", - helmPluginsDir: "/helm/data/plugins", - expectPath: "", - }, { - source: "https://github.com/jkroepke/helm-secrets", - helmPluginsDir: "/helm/data/plugins", - expectPath: "/helm/data/plugins/helm-secrets", - }, - } - - for _, tt := range tests { - - os.Setenv("HELM_PLUGINS", tt.helmPluginsDir) - baseIns := newBase(tt.source) - baseInsPath := baseIns.Path() - if baseInsPath != tt.expectPath { - t.Errorf("expected name %s, got %s", tt.expectPath, baseInsPath) - } - os.Unsetenv("HELM_PLUGINS") - } -} diff --git a/pkg/helm/pkg/plugin/installer/doc.go b/pkg/helm/pkg/plugin/installer/doc.go deleted file mode 100644 index 3e3b2ebe..00000000 --- a/pkg/helm/pkg/plugin/installer/doc.go +++ /dev/null @@ -1,17 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package installer provides an interface for installing Helm plugins. -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" diff --git a/pkg/helm/pkg/plugin/installer/http_installer.go b/pkg/helm/pkg/plugin/installer/http_installer.go deleted file mode 100644 index 790bfe3c..00000000 --- a/pkg/helm/pkg/plugin/installer/http_installer.go +++ /dev/null @@ -1,268 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "io" - "os" - "path" - "path/filepath" - "regexp" - "strings" - - securejoin "github.com/cyphar/filepath-securejoin" - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/intern/third_party/dep/fs" - "github.com/werf/nelm/pkg/helm/pkg/cli" - "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/plugin/cache" -) - -// HTTPInstaller installs plugins from an archive served by a web server. -type HTTPInstaller struct { - CacheDir string - PluginName string - base - extractor Extractor - getter getter.Getter -} - -// TarGzExtractor extracts gzip compressed tar archives -type TarGzExtractor struct{} - -// Extractor provides an interface for extracting archives -type Extractor interface { - Extract(buffer *bytes.Buffer, targetDir string) error -} - -// Extractors contains a map of suffixes and matching implementations of extractor to return -var Extractors = map[string]Extractor{ - ".tar.gz": &TarGzExtractor{}, - ".tgz": &TarGzExtractor{}, -} - -// Convert a media type to an extractor extension. -// -// This should be refactored in Helm 4, combined with the extension-based mechanism. -func mediaTypeToExtension(mt string) (string, bool) { - switch strings.ToLower(mt) { - case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar": - return ".tgz", true - default: - return "", false - } -} - -// NewExtractor creates a new extractor matching the source file name -func NewExtractor(source string) (Extractor, error) { - for suffix, extractor := range Extractors { - if strings.HasSuffix(source, suffix) { - return extractor, nil - } - } - return nil, errors.Errorf("no extractor implemented yet for %s", source) -} - -// NewHTTPInstaller creates a new HttpInstaller. -func NewHTTPInstaller(source string) (*HTTPInstaller, error) { - key, err := cache.Key(source) - if err != nil { - return nil, err - } - - extractor, err := NewExtractor(source) - if err != nil { - return nil, err - } - - get, err := getter.All(new(cli.EnvSettings)).ByScheme("http") - if err != nil { - return nil, err - } - - i := &HTTPInstaller{ - CacheDir: helmpath.CachePath("plugins", key), - PluginName: stripPluginName(filepath.Base(source)), - base: newBase(source), - extractor: extractor, - getter: get, - } - return i, nil -} - -// helper that relies on some sort of convention for plugin name (plugin-name-) -func stripPluginName(name string) string { - var strippedName string - for suffix := range Extractors { - if strings.HasSuffix(name, suffix) { - strippedName = strings.TrimSuffix(name, suffix) - break - } - } - re := regexp.MustCompile(`(.*)-[0-9]+\..*`) - return re.ReplaceAllString(strippedName, `$1`) -} - -// Install downloads and extracts the tarball into the cache directory -// and installs into the plugin directory. -// -// Implements Installer. -func (i *HTTPInstaller) Install() error { - pluginData, err := i.getter.Get(i.Source) - if err != nil { - return err - } - - if err := i.extractor.Extract(pluginData, i.CacheDir); err != nil { - return errors.Wrap(err, "extracting files from archive") - } - - if !isPlugin(i.CacheDir) { - return ErrMissingMetadata - } - - src, err := filepath.Abs(i.CacheDir) - if err != nil { - return err - } - - debug("copying %s to %s", src, i.Path()) - return fs.CopyDir(src, i.Path()) -} - -// Update updates a local repository -// Not implemented for now since tarball most likely will be packaged by version -func (i *HTTPInstaller) Update() error { - return errors.Errorf("method Update() not implemented for HttpInstaller") -} - -// Path is overridden because we want to join on the plugin name not the file name -func (i HTTPInstaller) Path() string { - if i.base.Source == "" { - return "" - } - return helmpath.DataPath("plugins", i.PluginName) -} - -// cleanJoin resolves dest as a subpath of root. -// -// This function runs several security checks on the path, generating an error if -// the supplied `dest` looks suspicious or would result in dubious behavior on the -// filesystem. -// -// cleanJoin assumes that any attempt by `dest` to break out of the CWD is an attempt -// to be malicious. (If you don't care about this, use the securejoin-filepath library.) -// It will emit an error if it detects paths that _look_ malicious, operating on the -// assumption that we don't actually want to do anything with files that already -// appear to be nefarious. -// -// - The character `:` is considered illegal because it is a separator on UNIX and a -// drive designator on Windows. -// - The path component `..` is considered suspicions, and therefore illegal -// - The character \ (backslash) is treated as a path separator and is converted to /. -// - Beginning a path with a path separator is illegal -// - Rudimentary symlink protects are offered by SecureJoin. -func cleanJoin(root, dest string) (string, error) { - - // On Windows, this is a drive separator. On UNIX-like, this is the path list separator. - // In neither case do we want to trust a TAR that contains these. - if strings.Contains(dest, ":") { - return "", errors.New("path contains ':', which is illegal") - } - - // The Go tar library does not convert separators for us. - // We assume here, as we do elsewhere, that `\\` means a Windows path. - dest = strings.ReplaceAll(dest, "\\", "/") - - // We want to alert the user that something bad was attempted. Cleaning it - // is not a good practice. - for _, part := range strings.Split(dest, "/") { - if part == ".." { - return "", errors.New("path contains '..', which is illegal") - } - } - - // If a path is absolute, the creator of the TAR is doing something shady. - if path.IsAbs(dest) { - return "", errors.New("path is absolute, which is illegal") - } - - // SecureJoin will do some cleaning, as well as some rudimentary checking of symlinks. - newpath, err := securejoin.SecureJoin(root, dest) - if err != nil { - return "", err - } - - return filepath.ToSlash(newpath), nil -} - -// Extract extracts compressed archives -// -// Implements Extractor. -func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { - uncompressedStream, err := gzip.NewReader(buffer) - if err != nil { - return err - } - - if err := os.MkdirAll(targetDir, 0755); err != nil { - return err - } - - tarReader := tar.NewReader(uncompressedStream) - for { - header, err := tarReader.Next() - if err == io.EOF { - break - } - if err != nil { - return err - } - - path, err := cleanJoin(targetDir, header.Name) - if err != nil { - return err - } - - switch header.Typeflag { - case tar.TypeDir: - if err := os.Mkdir(path, 0755); err != nil { - return err - } - case tar.TypeReg: - outFile, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) - if err != nil { - return err - } - if _, err := io.Copy(outFile, tarReader); err != nil { - outFile.Close() - return err - } - outFile.Close() - // We don't want to process these extension header files. - case tar.TypeXGlobalHeader, tar.TypeXHeader: - continue - default: - return errors.Errorf("unknown type: %b in %s", header.Typeflag, header.Name) - } - } - return nil -} diff --git a/pkg/helm/pkg/plugin/installer/http_installer_test.go b/pkg/helm/pkg/plugin/installer/http_installer_test.go deleted file mode 100644 index be598f2b..00000000 --- a/pkg/helm/pkg/plugin/installer/http_installer_test.go +++ /dev/null @@ -1,350 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "archive/tar" - "bytes" - "compress/gzip" - "encoding/base64" - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "syscall" - "testing" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/intern/test/ensure" - "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" -) - -var _ Installer = new(HTTPInstaller) - -// Fake http client -type TestHTTPGetter struct { - MockResponse *bytes.Buffer - MockError error -} - -func (t *TestHTTPGetter) Get(_ string, _ ...getter.Option) (*bytes.Buffer, error) { - return t.MockResponse, t.MockError -} - -// Fake plugin tarball data -var fakePluginB64 = "H4sIAKRj51kAA+3UX0vCUBgGcC9jn+Iwuk3Peza3GeyiUlJQkcogCOzgli7dJm4TvYk+a5+k479UqquUCJ/fLs549sLO2TnvWnJa9aXnjwujYdYLovxMhsPcfnHOLdNkOXthM/IVQQYjg2yyLLJ4kXGhLp5j0z3P41tZksqxmspL3B/O+j/XtZu1y8rdYzkOZRCxduKPk53ny6Wwz/GfIIf1As8lxzGJSmoHNLJZphKHG4YpTCE0wVk3DULfpSJ3DMMqkj3P5JfMYLdX1Vr9Ie/5E5cstcdC8K04iGLX5HaJuKpWL17F0TCIBi5pf/0pjtLhun5j3f9v6r7wfnI/H0eNp9d1/5P6Gez0vzo7wsoxfrAZbTny/o9k6J8z/VkO/LPlWdC1iVpbEEcq5nmeJ13LEtmbV0k2r2PrOs9PuuNglC5rL1Y5S/syXRQmutaNw1BGnnp8Wq3UG51WvX1da3bKtZtCN/R09DwAAAAAAAAAAAAAAAAAAADAb30AoMczDwAoAAA=" - -func TestStripName(t *testing.T) { - if stripPluginName("fake-plugin-0.0.1.tar.gz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } - if stripPluginName("fake-plugin-0.0.1.tgz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } - if stripPluginName("fake-plugin.tgz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } - if stripPluginName("fake-plugin.tar.gz") != "fake-plugin" { - t.Errorf("name does not match expected value") - } -} - -func mockArchiveServer() *httptest.Server { - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasSuffix(r.URL.Path, ".tar.gz") { - w.Header().Add("Content-Type", "text/html") - fmt.Fprintln(w, "broken") - return - } - w.Header().Add("Content-Type", "application/gzip") - fmt.Fprintln(w, "test") - })) -} - -func TestHTTPInstaller(t *testing.T) { - ensure.HelmHome(t) - - srv := mockArchiveServer() - defer srv.Close() - source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - - if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { - t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) - } - - i, err := NewForSource(source, "0.0.1") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - // ensure a HTTPInstaller was returned - httpInstaller, ok := i.(*HTTPInstaller) - if !ok { - t.Fatal("expected a HTTPInstaller") - } - - // inject fake http client responding with minimal plugin tarball - mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) - if err != nil { - t.Fatalf("Could not decode fake tgz plugin: %s", err) - } - - httpInstaller.getter = &TestHTTPGetter{ - MockResponse: bytes.NewBuffer(mockTgz), - } - - // install the plugin - if err := Install(i); err != nil { - t.Fatal(err) - } - if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { - t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) - } - - // Install again to test plugin exists error - if err := Install(i); err == nil { - t.Fatal("expected error for plugin exists, got none") - } else if err.Error() != "plugin already exists" { - t.Fatalf("expected error for plugin exists, got (%v)", err) - } - -} - -func TestHTTPInstallerNonExistentVersion(t *testing.T) { - ensure.HelmHome(t) - srv := mockArchiveServer() - defer srv.Close() - source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - - if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { - t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) - } - - i, err := NewForSource(source, "0.0.2") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - // ensure a HTTPInstaller was returned - httpInstaller, ok := i.(*HTTPInstaller) - if !ok { - t.Fatal("expected a HTTPInstaller") - } - - // inject fake http client responding with error - httpInstaller.getter = &TestHTTPGetter{ - MockError: errors.Errorf("failed to download plugin for some reason"), - } - - // attempt to install the plugin - if err := Install(i); err == nil { - t.Fatal("expected error from http client") - } - -} - -func TestHTTPInstallerUpdate(t *testing.T) { - srv := mockArchiveServer() - defer srv.Close() - source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - ensure.HelmHome(t) - - if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { - t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) - } - - i, err := NewForSource(source, "0.0.1") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - // ensure a HTTPInstaller was returned - httpInstaller, ok := i.(*HTTPInstaller) - if !ok { - t.Fatal("expected a HTTPInstaller") - } - - // inject fake http client responding with minimal plugin tarball - mockTgz, err := base64.StdEncoding.DecodeString(fakePluginB64) - if err != nil { - t.Fatalf("Could not decode fake tgz plugin: %s", err) - } - - httpInstaller.getter = &TestHTTPGetter{ - MockResponse: bytes.NewBuffer(mockTgz), - } - - // install the plugin before updating - if err := Install(i); err != nil { - t.Fatal(err) - } - if i.Path() != helmpath.DataPath("plugins", "fake-plugin") { - t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/fake-plugin', got %q", i.Path()) - } - - // Update plugin, should fail because it is not implemented - if err := Update(i); err == nil { - t.Fatal("update method not implemented for http installer") - } -} - -func TestExtract(t *testing.T) { - source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" - - tempDir := t.TempDir() - - // Set the umask to default open permissions so we can actually test - oldmask := syscall.Umask(0000) - defer func() { - syscall.Umask(oldmask) - }() - - // Write a tarball to a buffer for us to extract - var tarbuf bytes.Buffer - tw := tar.NewWriter(&tarbuf) - var files = []struct { - Name, Body string - Mode int64 - }{ - {"plugin.yaml", "plugin metadata", 0600}, - {"README.md", "some text", 0777}, - } - for _, file := range files { - hdr := &tar.Header{ - Name: file.Name, - Typeflag: tar.TypeReg, - Mode: file.Mode, - Size: int64(len(file.Body)), - } - if err := tw.WriteHeader(hdr); err != nil { - t.Fatal(err) - } - if _, err := tw.Write([]byte(file.Body)); err != nil { - t.Fatal(err) - } - } - - // Add pax global headers. This should be ignored. - // Note the PAX header that isn't global cannot be written using WriteHeader. - // Details are in the internal Go function for the tar packaged named - // allowedFormats. For a TypeXHeader it will return a message stating - // "cannot manually encode TypeXHeader, TypeGNULongName, or TypeGNULongLink headers" - if err := tw.WriteHeader(&tar.Header{ - Name: "pax_global_header", - Typeflag: tar.TypeXGlobalHeader, - }); err != nil { - t.Fatal(err) - } - - if err := tw.Close(); err != nil { - t.Fatal(err) - } - - var buf bytes.Buffer - gz := gzip.NewWriter(&buf) - if _, err := gz.Write(tarbuf.Bytes()); err != nil { - t.Fatal(err) - } - gz.Close() - // END tarball creation - - extractor, err := NewExtractor(source) - if err != nil { - t.Fatal(err) - } - - if err = extractor.Extract(&buf, tempDir); err != nil { - t.Fatalf("Did not expect error but got error: %v", err) - } - - pluginYAMLFullPath := filepath.Join(tempDir, "plugin.yaml") - if info, err := os.Stat(pluginYAMLFullPath); err != nil { - if os.IsNotExist(err) { - t.Fatalf("Expected %s to exist but doesn't", pluginYAMLFullPath) - } - t.Fatal(err) - } else if info.Mode().Perm() != 0600 { - t.Fatalf("Expected %s to have 0600 mode it but has %o", pluginYAMLFullPath, info.Mode().Perm()) - } - - readmeFullPath := filepath.Join(tempDir, "README.md") - if info, err := os.Stat(readmeFullPath); err != nil { - if os.IsNotExist(err) { - t.Fatalf("Expected %s to exist but doesn't", readmeFullPath) - } - t.Fatal(err) - } else if info.Mode().Perm() != 0777 { - t.Fatalf("Expected %s to have 0777 mode it but has %o", readmeFullPath, info.Mode().Perm()) - } - -} - -func TestCleanJoin(t *testing.T) { - for i, fixture := range []struct { - path string - expect string - expectError bool - }{ - {"foo/bar.txt", "/tmp/foo/bar.txt", false}, - {"/foo/bar.txt", "", true}, - {"./foo/bar.txt", "/tmp/foo/bar.txt", false}, - {"./././././foo/bar.txt", "/tmp/foo/bar.txt", false}, - {"../../../../foo/bar.txt", "", true}, - {"foo/../../../../bar.txt", "", true}, - {"c:/foo/bar.txt", "/tmp/c:/foo/bar.txt", true}, - {"foo\\bar.txt", "/tmp/foo/bar.txt", false}, - {"c:\\foo\\bar.txt", "", true}, - } { - out, err := cleanJoin("/tmp", fixture.path) - if err != nil { - if !fixture.expectError { - t.Errorf("Test %d: Path was not cleaned: %s", i, err) - } - continue - } - if fixture.expect != out { - t.Errorf("Test %d: Expected %q but got %q", i, fixture.expect, out) - } - } - -} - -func TestMediaTypeToExtension(t *testing.T) { - - for mt, shouldPass := range map[string]bool{ - "": false, - "application/gzip": true, - "application/x-gzip": true, - "application/x-tgz": true, - "application/x-gtar": true, - "application/json": false, - } { - ext, ok := mediaTypeToExtension(mt) - if ok != shouldPass { - t.Errorf("Media type %q failed test", mt) - } - if shouldPass && ext == "" { - t.Errorf("Expected an extension but got empty string") - } - if !shouldPass && len(ext) != 0 { - t.Error("Expected extension to be empty for unrecognized type") - } - } -} diff --git a/pkg/helm/pkg/plugin/installer/installer.go b/pkg/helm/pkg/plugin/installer/installer.go deleted file mode 100644 index 5b124865..00000000 --- a/pkg/helm/pkg/plugin/installer/installer.go +++ /dev/null @@ -1,135 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer - -import ( - "fmt" - "log" - "net/http" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/pkg/plugin" -) - -// ErrMissingMetadata indicates that plugin.yaml is missing. -var ErrMissingMetadata = errors.New("plugin metadata (plugin.yaml) missing") - -// Debug enables verbose output. -var Debug bool - -// Installer provides an interface for installing helm client plugins. -type Installer interface { - // Install adds a plugin. - Install() error - // Path is the directory of the installed plugin. - Path() string - // Update updates a plugin. - Update() error -} - -// Install installs a plugin. -func Install(i Installer) error { - if err := os.MkdirAll(filepath.Dir(i.Path()), 0755); err != nil { - return err - } - if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) { - return errors.New("plugin already exists") - } - return i.Install() -} - -// Update updates a plugin. -func Update(i Installer) error { - if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) { - return errors.New("plugin does not exist") - } - return i.Update() -} - -// NewForSource determines the correct Installer for the given source. -func NewForSource(source, version string) (Installer, error) { - // Check if source is a local directory - if isLocalReference(source) { - return NewLocalInstaller(source) - } else if isRemoteHTTPArchive(source) { - return NewHTTPInstaller(source) - } - return NewVCSInstaller(source, version) -} - -// FindSource determines the correct Installer for the given source. -func FindSource(location string) (Installer, error) { - installer, err := existingVCSRepo(location) - if err != nil && err.Error() == "Cannot detect VCS" { - return installer, errors.New("cannot get information about plugin source") - } - return installer, err -} - -// isLocalReference checks if the source exists on the filesystem. -func isLocalReference(source string) bool { - _, err := os.Stat(source) - return err == nil -} - -// isRemoteHTTPArchive checks if the source is a http/https url and is an archive -// -// It works by checking whether the source looks like a URL and, if it does, running a -// HEAD operation to see if the remote resource is a file that we understand. -func isRemoteHTTPArchive(source string) bool { - if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { - res, err := http.Head(source) - if err != nil { - // If we get an error at the network layer, we can't install it. So - // we return false. - return false - } - - // Next, we look for the content type or content disposition headers to see - // if they have matching extractors. - contentType := res.Header.Get("content-type") - foundSuffix, ok := mediaTypeToExtension(contentType) - if !ok { - // Media type not recognized - return false - } - - for suffix := range Extractors { - if strings.HasSuffix(foundSuffix, suffix) { - return true - } - } - } - return false -} - -// isPlugin checks if the directory contains a plugin.yaml file. -func isPlugin(dirname string) bool { - _, err := os.Stat(filepath.Join(dirname, plugin.PluginFileName)) - return err == nil -} - -var logger = log.New(os.Stderr, "[debug] ", log.Lshortfile) - -func debug(format string, args ...interface{}) { - if Debug { - logger.Output(2, fmt.Sprintf(format, args...)) - } -} diff --git a/pkg/helm/pkg/plugin/installer/installer_test.go b/pkg/helm/pkg/plugin/installer/installer_test.go deleted file mode 100644 index a1146492..00000000 --- a/pkg/helm/pkg/plugin/installer/installer_test.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer - -import "testing" - -func TestIsRemoteHTTPArchive(t *testing.T) { - srv := mockArchiveServer() - defer srv.Close() - source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz" - - if isRemoteHTTPArchive("/not/a/URL") { - t.Errorf("Expected non-URL to return false") - } - - if isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz") { - t.Errorf("Bad URL should not have succeeded.") - } - - if !isRemoteHTTPArchive(source) { - t.Errorf("Expected %q to be a valid archive URL", source) - } - - if isRemoteHTTPArchive(source + "-not-an-extension") { - t.Error("Expected media type match to fail") - } -} diff --git a/pkg/helm/pkg/plugin/installer/local_installer.go b/pkg/helm/pkg/plugin/installer/local_installer.go deleted file mode 100644 index 759df38b..00000000 --- a/pkg/helm/pkg/plugin/installer/local_installer.go +++ /dev/null @@ -1,68 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "os" - "path/filepath" - - "github.com/pkg/errors" -) - -// ErrPluginNotAFolder indicates that the plugin path is not a folder. -var ErrPluginNotAFolder = errors.New("expected plugin to be a folder") - -// LocalInstaller installs plugins from the filesystem. -type LocalInstaller struct { - base -} - -// NewLocalInstaller creates a new LocalInstaller. -func NewLocalInstaller(source string) (*LocalInstaller, error) { - src, err := filepath.Abs(source) - if err != nil { - return nil, errors.Wrap(err, "unable to get absolute path to plugin") - } - i := &LocalInstaller{ - base: newBase(src), - } - return i, nil -} - -// Install creates a symlink to the plugin directory. -// -// Implements Installer. -func (i *LocalInstaller) Install() error { - stat, err := os.Stat(i.Source) - if err != nil { - return err - } - if !stat.IsDir() { - return ErrPluginNotAFolder - } - - if !isPlugin(i.Source) { - return ErrMissingMetadata - } - debug("symlinking %s to %s", i.Source, i.Path()) - return os.Symlink(i.Source, i.Path()) -} - -// Update updates a local repository -func (i *LocalInstaller) Update() error { - debug("local repository is auto-updated") - return nil -} diff --git a/pkg/helm/pkg/plugin/installer/local_installer_test.go b/pkg/helm/pkg/plugin/installer/local_installer_test.go deleted file mode 100644 index c096bec0..00000000 --- a/pkg/helm/pkg/plugin/installer/local_installer_test.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "os" - "path/filepath" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/helmpath" -) - -var _ Installer = new(LocalInstaller) - -func TestLocalInstaller(t *testing.T) { - // Make a temp dir - tdir := t.TempDir() - if err := os.WriteFile(filepath.Join(tdir, "plugin.yaml"), []byte{}, 0644); err != nil { - t.Fatal(err) - } - - source := "../testdata/plugdir/good/echo" - i, err := NewForSource(source, "") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - if err := Install(i); err != nil { - t.Fatal(err) - } - - if i.Path() != helmpath.DataPath("plugins", "echo") { - t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) - } - defer os.RemoveAll(filepath.Dir(helmpath.DataPath())) // helmpath.DataPath is like /tmp/helm013130971/helm -} - -func TestLocalInstallerNotAFolder(t *testing.T) { - source := "../testdata/plugdir/good/echo/plugin.yaml" - i, err := NewForSource(source, "") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - err = Install(i) - if err == nil { - t.Fatal("expected error") - } - if err != ErrPluginNotAFolder { - t.Fatalf("expected error to equal: %q", err) - } -} diff --git a/pkg/helm/pkg/plugin/installer/vcs_installer.go b/pkg/helm/pkg/plugin/installer/vcs_installer.go deleted file mode 100644 index 59edb998..00000000 --- a/pkg/helm/pkg/plugin/installer/vcs_installer.go +++ /dev/null @@ -1,176 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "os" - "sort" - - "github.com/Masterminds/semver/v3" - "github.com/Masterminds/vcs" - "github.com/pkg/errors" - - "github.com/werf/nelm/pkg/helm/intern/third_party/dep/fs" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/plugin/cache" -) - -// VCSInstaller installs plugins from remote a repository. -type VCSInstaller struct { - Repo vcs.Repo - Version string - base -} - -func existingVCSRepo(location string) (Installer, error) { - repo, err := vcs.NewRepo("", location) - if err != nil { - return nil, err - } - i := &VCSInstaller{ - Repo: repo, - base: newBase(repo.Remote()), - } - return i, nil -} - -// NewVCSInstaller creates a new VCSInstaller. -func NewVCSInstaller(source, version string) (*VCSInstaller, error) { - key, err := cache.Key(source) - if err != nil { - return nil, err - } - cachedpath := helmpath.CachePath("plugins", key) - repo, err := vcs.NewRepo(source, cachedpath) - if err != nil { - return nil, err - } - i := &VCSInstaller{ - Repo: repo, - Version: version, - base: newBase(source), - } - return i, err -} - -// Install clones a remote repository and installs into the plugin directory. -// -// Implements Installer. -func (i *VCSInstaller) Install() error { - if err := i.sync(i.Repo); err != nil { - return err - } - - ref, err := i.solveVersion(i.Repo) - if err != nil { - return err - } - if ref != "" { - if err := i.setVersion(i.Repo, ref); err != nil { - return err - } - } - - if !isPlugin(i.Repo.LocalPath()) { - return ErrMissingMetadata - } - - debug("copying %s to %s", i.Repo.LocalPath(), i.Path()) - return fs.CopyDir(i.Repo.LocalPath(), i.Path()) -} - -// Update updates a remote repository -func (i *VCSInstaller) Update() error { - debug("updating %s", i.Repo.Remote()) - if i.Repo.IsDirty() { - return errors.New("plugin repo was modified") - } - if err := i.Repo.Update(); err != nil { - return err - } - if !isPlugin(i.Repo.LocalPath()) { - return ErrMissingMetadata - } - return nil -} - -func (i *VCSInstaller) solveVersion(repo vcs.Repo) (string, error) { - if i.Version == "" { - return "", nil - } - - if repo.IsReference(i.Version) { - return i.Version, nil - } - - // Create the constraint first to make sure it's valid before - // working on the repo. - constraint, err := semver.NewConstraint(i.Version) - if err != nil { - return "", err - } - - // Get the tags - refs, err := repo.Tags() - if err != nil { - return "", err - } - debug("found refs: %s", refs) - - // Convert and filter the list to semver.Version instances - semvers := getSemVers(refs) - - // Sort semver list - sort.Sort(sort.Reverse(semver.Collection(semvers))) - for _, v := range semvers { - if constraint.Check(v) { - // If the constraint passes get the original reference - ver := v.Original() - debug("setting to %s", ver) - return ver, nil - } - } - - return "", errors.Errorf("requested version %q does not exist for plugin %q", i.Version, i.Repo.Remote()) -} - -// setVersion attempts to checkout the version -func (i *VCSInstaller) setVersion(repo vcs.Repo, ref string) error { - debug("setting version to %q", i.Version) - return repo.UpdateVersion(ref) -} - -// sync will clone or update a remote repo. -func (i *VCSInstaller) sync(repo vcs.Repo) error { - if _, err := os.Stat(repo.LocalPath()); os.IsNotExist(err) { - debug("cloning %s to %s", repo.Remote(), repo.LocalPath()) - return repo.Get() - } - debug("updating %s", repo.Remote()) - return repo.Update() -} - -// Filter a list of versions to only included semantic versions. The response -// is a mapping of the original version to the semantic version. -func getSemVers(refs []string) []*semver.Version { - var sv []*semver.Version - for _, r := range refs { - if v, err := semver.NewVersion(r); err == nil { - sv = append(sv, v) - } - } - return sv -} diff --git a/pkg/helm/pkg/plugin/installer/vcs_installer_test.go b/pkg/helm/pkg/plugin/installer/vcs_installer_test.go deleted file mode 100644 index bdacd0d0..00000000 --- a/pkg/helm/pkg/plugin/installer/vcs_installer_test.go +++ /dev/null @@ -1,181 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package installer // import "helm.sh/helm/v3/pkg/plugin/installer" - -import ( - "fmt" - "os" - "path/filepath" - "testing" - - "github.com/Masterminds/vcs" - - "github.com/werf/nelm/pkg/helm/intern/test/ensure" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" -) - -var _ Installer = new(VCSInstaller) - -type testRepo struct { - local, remote, current string - tags, branches []string - err error - vcs.Repo -} - -func (r *testRepo) LocalPath() string { return r.local } -func (r *testRepo) Remote() string { return r.remote } -func (r *testRepo) Update() error { return r.err } -func (r *testRepo) Get() error { return r.err } -func (r *testRepo) IsReference(string) bool { return false } -func (r *testRepo) Tags() ([]string, error) { return r.tags, r.err } -func (r *testRepo) Branches() ([]string, error) { return r.branches, r.err } -func (r *testRepo) UpdateVersion(version string) error { - r.current = version - return r.err -} - -func TestVCSInstaller(t *testing.T) { - ensure.HelmHome(t) - - if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { - t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) - } - - source := "https://github.com/adamreese/helm-env" - testRepoPath, _ := filepath.Abs("../testdata/plugdir/good/echo") - repo := &testRepo{ - local: testRepoPath, - tags: []string{"0.1.0", "0.1.1"}, - } - - i, err := NewForSource(source, "~0.1.0") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - // ensure a VCSInstaller was returned - vcsInstaller, ok := i.(*VCSInstaller) - if !ok { - t.Fatal("expected a VCSInstaller") - } - - // set the testRepo in the VCSInstaller - vcsInstaller.Repo = repo - - if err := Install(i); err != nil { - t.Fatal(err) - } - if repo.current != "0.1.1" { - t.Fatalf("expected version '0.1.1', got %q", repo.current) - } - if i.Path() != helmpath.DataPath("plugins", "helm-env") { - t.Fatalf("expected path '$XDG_CONFIG_HOME/helm/plugins/helm-env', got %q", i.Path()) - } - - // Install again to test plugin exists error - if err := Install(i); err == nil { - t.Fatalf("expected error for plugin exists, got none") - } else if err.Error() != "plugin already exists" { - t.Fatalf("expected error for plugin exists, got (%v)", err) - } - - // Testing FindSource method, expect error because plugin code is not a cloned repository - if _, err := FindSource(i.Path()); err == nil { - t.Fatalf("expected error for inability to find plugin source, got none") - } else if err.Error() != "cannot get information about plugin source" { - t.Fatalf("expected error for inability to find plugin source, got (%v)", err) - } -} - -func TestVCSInstallerNonExistentVersion(t *testing.T) { - ensure.HelmHome(t) - - source := "https://github.com/adamreese/helm-env" - version := "0.2.0" - - i, err := NewForSource(source, version) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - // ensure a VCSInstaller was returned - if _, ok := i.(*VCSInstaller); !ok { - t.Fatal("expected a VCSInstaller") - } - - if err := Install(i); err == nil { - t.Fatalf("expected error for version does not exists, got none") - } else if err.Error() != fmt.Sprintf("requested version %q does not exist for plugin %q", version, source) { - t.Fatalf("expected error for version does not exists, got (%v)", err) - } -} -func TestVCSInstallerUpdate(t *testing.T) { - ensure.HelmHome(t) - - source := "https://github.com/adamreese/helm-env" - - i, err := NewForSource(source, "") - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - // ensure a VCSInstaller was returned - if _, ok := i.(*VCSInstaller); !ok { - t.Fatal("expected a VCSInstaller") - } - - if err := Update(i); err == nil { - t.Fatal("expected error for plugin does not exist, got none") - } else if err.Error() != "plugin does not exist" { - t.Fatalf("expected error for plugin does not exist, got (%v)", err) - } - - // Install plugin before update - if err := Install(i); err != nil { - t.Fatal(err) - } - - // Test FindSource method for positive result - pluginInfo, err := FindSource(i.Path()) - if err != nil { - t.Fatal(err) - } - - vcsInstaller := pluginInfo.(*VCSInstaller) - - repoRemote := vcsInstaller.Repo.Remote() - if repoRemote != source { - t.Fatalf("invalid source found, expected %q got %q", source, repoRemote) - } - - // Update plugin - if err := Update(i); err != nil { - t.Fatal(err) - } - - // Test update failure - if err := os.Remove(filepath.Join(vcsInstaller.Repo.LocalPath(), "plugin.yaml")); err != nil { - t.Fatal(err) - } - // Testing update for error - if err := Update(vcsInstaller); err == nil { - t.Fatalf("expected error for plugin modified, got none") - } else if err.Error() != "plugin repo was modified" { - t.Fatalf("expected error for plugin modified, got (%v)", err) - } - -} diff --git a/pkg/helm/pkg/plugin/plugin.go b/pkg/helm/pkg/plugin/plugin.go deleted file mode 100644 index 4e7b9f8a..00000000 --- a/pkg/helm/pkg/plugin/plugin.go +++ /dev/null @@ -1,288 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package plugin // import "helm.sh/helm/v3/pkg/plugin" - -import ( - "fmt" - "os" - "path/filepath" - "regexp" - "runtime" - "strings" - "unicode" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/cli" -) - -const PluginFileName = "plugin.yaml" - -// Downloaders represents the plugins capability if it can retrieve -// charts from special sources -type Downloaders struct { - // Protocols are the list of schemes from the charts URL. - Protocols []string `json:"protocols"` - // Command is the executable path with which the plugin performs - // the actual download for the corresponding Protocols - Command string `json:"command"` -} - -// PlatformCommand represents a command for a particular operating system and architecture -type PlatformCommand struct { - OperatingSystem string `json:"os"` - Architecture string `json:"arch"` - Command string `json:"command"` -} - -// Metadata describes a plugin. -// -// This is the plugin equivalent of a chart.Metadata. -type Metadata struct { - // Name is the name of the plugin - Name string `json:"name"` - - // Version is a SemVer 2 version of the plugin. - Version string `json:"version"` - - // Usage is the single-line usage text shown in help - Usage string `json:"usage"` - - // Description is a long description shown in places like `helm help` - Description string `json:"description"` - - // Command is the command, as a single string. - // - // The command will be passed through environment expansion, so env vars can - // be present in this command. Unless IgnoreFlags is set, this will - // also merge the flags passed from Helm. - // - // Note that command is not executed in a shell. To do so, we suggest - // pointing the command to a shell script. - // - // The following rules will apply to processing commands: - // - If platformCommand is present, it will be searched first - // - If both OS and Arch match the current platform, search will stop and the command will be executed - // - If OS matches and there is no more specific match, the command will be executed - // - If no OS/Arch match is found, the default command will be executed - // - If no command is present and no matches are found in platformCommand, Helm will exit with an error - PlatformCommand []PlatformCommand `json:"platformCommand"` - Command string `json:"command"` - - // IgnoreFlags ignores any flags passed in from Helm - // - // For example, if the plugin is invoked as `helm --debug myplugin`, if this - // is false, `--debug` will be appended to `--command`. If this is true, - // the `--debug` flag will be discarded. - IgnoreFlags bool `json:"ignoreFlags"` - - // Hooks are commands that will run on events. - Hooks Hooks - - // Downloaders field is used if the plugin supply downloader mechanism - // for special protocols. - Downloaders []Downloaders `json:"downloaders"` - - // UseTunnelDeprecated indicates that this command needs a tunnel. - // Setting this will cause a number of side effects, such as the - // automatic setting of HELM_HOST. - // DEPRECATED and unused, but retained for backwards compatibility with Helm 2 plugins. Remove in Helm 4 - UseTunnelDeprecated bool `json:"useTunnel,omitempty"` -} - -// Plugin represents a plugin. -type Plugin struct { - // Metadata is a parsed representation of a plugin.yaml - Metadata *Metadata - // Dir is the string path to the directory that holds the plugin. - Dir string -} - -// The following rules will apply to processing the Plugin.PlatformCommand.Command: -// - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution -// - If OS matches and there is no more specific match, the command will be prepared for execution -// - If no OS/Arch match is found, return nil -func getPlatformCommand(cmds []PlatformCommand) []string { - var command []string - eq := strings.EqualFold - for _, c := range cmds { - if eq(c.OperatingSystem, runtime.GOOS) { - command = strings.Split(c.Command, " ") - } - if eq(c.OperatingSystem, runtime.GOOS) && eq(c.Architecture, runtime.GOARCH) { - return strings.Split(c.Command, " ") - } - } - return command -} - -// PrepareCommand takes a Plugin.PlatformCommand.Command, a Plugin.Command and will applying the following processing: -// - If platformCommand is present, it will be searched first -// - If both OS and Arch match the current platform, search will stop and the command will be prepared for execution -// - If OS matches and there is no more specific match, the command will be prepared for execution -// - If no OS/Arch match is found, the default command will be prepared for execution -// - If no command is present and no matches are found in platformCommand, will exit with an error -// -// It merges extraArgs into any arguments supplied in the plugin. It -// returns the name of the command and an args array. -// -// The result is suitable to pass to exec.Command. -func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) { - var parts []string - platCmdLen := len(p.Metadata.PlatformCommand) - if platCmdLen > 0 { - parts = getPlatformCommand(p.Metadata.PlatformCommand) - } - if platCmdLen == 0 || parts == nil { - parts = strings.Split(p.Metadata.Command, " ") - } - if len(parts) == 0 || parts[0] == "" { - return "", nil, fmt.Errorf("no plugin command is applicable") - } - - main := os.ExpandEnv(parts[0]) - baseArgs := []string{} - if len(parts) > 1 { - for _, cmdpart := range parts[1:] { - cmdexp := os.ExpandEnv(cmdpart) - baseArgs = append(baseArgs, cmdexp) - } - } - if !p.Metadata.IgnoreFlags { - baseArgs = append(baseArgs, extraArgs...) - } - return main, baseArgs, nil -} - -// validPluginName is a regular expression that validates plugin names. -// -// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, ​_​ and ​-. -var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$") - -// validatePluginData validates a plugin's YAML data. -func validatePluginData(plug *Plugin, filepath string) error { - // When metadata section missing, initialize with no data - if plug.Metadata == nil { - plug.Metadata = &Metadata{} - } - if !validPluginName.MatchString(plug.Metadata.Name) { - return fmt.Errorf("invalid plugin name at %q", filepath) - } - plug.Metadata.Usage = sanitizeString(plug.Metadata.Usage) - - // We could also validate SemVer, executable, and other fields should we so choose. - return nil -} - -// sanitizeString normalize spaces and removes non-printable characters. -func sanitizeString(str string) string { - return strings.Map(func(r rune) rune { - if unicode.IsSpace(r) { - return ' ' - } - if unicode.IsPrint(r) { - return r - } - return -1 - }, str) -} - -func detectDuplicates(plugs []*Plugin) error { - names := map[string]string{} - - for _, plug := range plugs { - if oldpath, ok := names[plug.Metadata.Name]; ok { - return fmt.Errorf( - "two plugins claim the name %q at %q and %q", - plug.Metadata.Name, - oldpath, - plug.Dir, - ) - } - names[plug.Metadata.Name] = plug.Dir - } - - return nil -} - -// LoadDir loads a plugin from the given directory. -func LoadDir(dirname string) (*Plugin, error) { - pluginfile := filepath.Join(dirname, PluginFileName) - data, err := os.ReadFile(pluginfile) - if err != nil { - return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile) - } - - plug := &Plugin{Dir: dirname} - if err := yaml.UnmarshalStrict(data, &plug.Metadata); err != nil { - return nil, errors.Wrapf(err, "failed to load plugin at %q", pluginfile) - } - return plug, validatePluginData(plug, pluginfile) -} - -// LoadAll loads all plugins found beneath the base directory. -// -// This scans only one directory level. -func LoadAll(basedir string) ([]*Plugin, error) { - plugins := []*Plugin{} - // We want basedir/*/plugin.yaml - scanpath := filepath.Join(basedir, "*", PluginFileName) - matches, err := filepath.Glob(scanpath) - if err != nil { - return plugins, errors.Wrapf(err, "failed to find plugins in %q", scanpath) - } - - if matches == nil { - return plugins, nil - } - - for _, yaml := range matches { - dir := filepath.Dir(yaml) - p, err := LoadDir(dir) - if err != nil { - return plugins, err - } - plugins = append(plugins, p) - } - return plugins, detectDuplicates(plugins) -} - -// FindPlugins returns a list of YAML files that describe plugins. -func FindPlugins(plugdirs string) ([]*Plugin, error) { - found := []*Plugin{} - // Let's get all UNIXy and allow path separators - for _, p := range filepath.SplitList(plugdirs) { - matches, err := LoadAll(p) - if err != nil { - return matches, err - } - found = append(found, matches...) - } - return found, nil -} - -// SetupPluginEnv prepares os.Env for plugins. It operates on os.Env because -// the plugin subsystem itself needs access to the environment variables -// created here. -func SetupPluginEnv(settings *cli.EnvSettings, name, base string) { - env := settings.EnvVars() - env["HELM_PLUGIN_NAME"] = name - env["HELM_PLUGIN_DIR"] = base - for key, val := range env { - os.Setenv(key, val) - } -} diff --git a/pkg/helm/pkg/plugin/plugin_test.go b/pkg/helm/pkg/plugin/plugin_test.go deleted file mode 100644 index d0ad61e0..00000000 --- a/pkg/helm/pkg/plugin/plugin_test.go +++ /dev/null @@ -1,407 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package plugin // import "helm.sh/helm/v3/pkg/plugin" - -import ( - "fmt" - "os" - "path/filepath" - "reflect" - "runtime" - "testing" - - "github.com/werf/nelm/pkg/helm/pkg/cli" -) - -func checkCommand(p *Plugin, extraArgs []string, osStrCmp string, t *testing.T) { - cmd, args, err := p.PrepareCommand(extraArgs) - if err != nil { - t.Fatal(err) - } - if cmd != "echo" { - t.Fatalf("Expected echo, got %q", cmd) - } - - if l := len(args); l != 5 { - t.Fatalf("expected 5 args, got %d", l) - } - - expect := []string{"-n", osStrCmp, "--debug", "--foo", "bar"} - for i := 0; i < len(args); i++ { - if expect[i] != args[i] { - t.Errorf("Expected arg=%q, got %q", expect[i], args[i]) - } - } - - // Test with IgnoreFlags. This should omit --debug, --foo, bar - p.Metadata.IgnoreFlags = true - cmd, args, err = p.PrepareCommand(extraArgs) - if err != nil { - t.Fatal(err) - } - if cmd != "echo" { - t.Fatalf("Expected echo, got %q", cmd) - } - if l := len(args); l != 2 { - t.Fatalf("expected 2 args, got %d", l) - } - expect = []string{"-n", osStrCmp} - for i := 0; i < len(args); i++ { - if expect[i] != args[i] { - t.Errorf("Expected arg=%q, got %q", expect[i], args[i]) - } - } -} - -func TestPrepareCommand(t *testing.T) { - p := &Plugin{ - Dir: "/tmp", // Unused - Metadata: &Metadata{ - Name: "test", - Command: "echo -n foo", - }, - } - argv := []string{"--debug", "--foo", "bar"} - - checkCommand(p, argv, "foo", t) -} - -func TestPlatformPrepareCommand(t *testing.T) { - p := &Plugin{ - Dir: "/tmp", // Unused - Metadata: &Metadata{ - Name: "test", - Command: "echo -n os-arch", - PlatformCommand: []PlatformCommand{ - {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"}, - {OperatingSystem: "linux", Architecture: "amd64", Command: "echo -n linux-amd64"}, - {OperatingSystem: "linux", Architecture: "arm64", Command: "echo -n linux-arm64"}, - {OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"}, - {OperatingSystem: "linux", Architecture: "s390x", Command: "echo -n linux-s390x"}, - {OperatingSystem: "linux", Architecture: "riscv64", Command: "echo -n linux-riscv64"}, - {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, - }, - }, - } - var osStrCmp string - os := runtime.GOOS - arch := runtime.GOARCH - if os == "linux" && arch == "386" { - osStrCmp = "linux-386" - } else if os == "linux" && arch == "amd64" { - osStrCmp = "linux-amd64" - } else if os == "linux" && arch == "arm64" { - osStrCmp = "linux-arm64" - } else if os == "linux" && arch == "ppc64le" { - osStrCmp = "linux-ppc64le" - } else if os == "linux" && arch == "s390x" { - osStrCmp = "linux-s390x" - } else if os == "linux" && arch == "riscv64" { - osStrCmp = "linux-riscv64" - } else if os == "windows" && arch == "amd64" { - osStrCmp = "win-64" - } else { - osStrCmp = "os-arch" - } - - argv := []string{"--debug", "--foo", "bar"} - checkCommand(p, argv, osStrCmp, t) -} - -func TestPartialPlatformPrepareCommand(t *testing.T) { - p := &Plugin{ - Dir: "/tmp", // Unused - Metadata: &Metadata{ - Name: "test", - Command: "echo -n os-arch", - PlatformCommand: []PlatformCommand{ - {OperatingSystem: "linux", Architecture: "386", Command: "echo -n linux-386"}, - {OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"}, - }, - }, - } - var osStrCmp string - os := runtime.GOOS - arch := runtime.GOARCH - if os == "linux" { - osStrCmp = "linux-386" - } else if os == "windows" && arch == "amd64" { - osStrCmp = "win-64" - } else { - osStrCmp = "os-arch" - } - - argv := []string{"--debug", "--foo", "bar"} - checkCommand(p, argv, osStrCmp, t) -} - -func TestNoPrepareCommand(t *testing.T) { - p := &Plugin{ - Dir: "/tmp", // Unused - Metadata: &Metadata{ - Name: "test", - }, - } - argv := []string{"--debug", "--foo", "bar"} - - _, _, err := p.PrepareCommand(argv) - if err == nil { - t.Fatalf("Expected error to be returned") - } -} - -func TestNoMatchPrepareCommand(t *testing.T) { - p := &Plugin{ - Dir: "/tmp", // Unused - Metadata: &Metadata{ - Name: "test", - PlatformCommand: []PlatformCommand{ - {OperatingSystem: "no-os", Architecture: "amd64", Command: "echo -n linux-386"}, - }, - }, - } - argv := []string{"--debug", "--foo", "bar"} - - if _, _, err := p.PrepareCommand(argv); err == nil { - t.Fatalf("Expected error to be returned") - } -} - -func TestLoadDir(t *testing.T) { - dirname := "testdata/plugdir/good/hello" - plug, err := LoadDir(dirname) - if err != nil { - t.Fatalf("error loading Hello plugin: %s", err) - } - - if plug.Dir != dirname { - t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir) - } - - expect := &Metadata{ - Name: "hello", - Version: "0.1.0", - Usage: "usage", - Description: "description", - Command: "$HELM_PLUGIN_DIR/hello.sh", - IgnoreFlags: true, - Hooks: map[string]string{ - Install: "echo installing...", - }, - } - - if !reflect.DeepEqual(expect, plug.Metadata) { - t.Fatalf("Expected plugin metadata %v, got %v", expect, plug.Metadata) - } -} - -func TestLoadDirDuplicateEntries(t *testing.T) { - dirname := "testdata/plugdir/bad/duplicate-entries" - if _, err := LoadDir(dirname); err == nil { - t.Errorf("successfully loaded plugin with duplicate entries when it should've failed") - } -} - -func TestDownloader(t *testing.T) { - dirname := "testdata/plugdir/good/downloader" - plug, err := LoadDir(dirname) - if err != nil { - t.Fatalf("error loading Hello plugin: %s", err) - } - - if plug.Dir != dirname { - t.Fatalf("Expected dir %q, got %q", dirname, plug.Dir) - } - - expect := &Metadata{ - Name: "downloader", - Version: "1.2.3", - Usage: "usage", - Description: "download something", - Command: "echo Hello", - Downloaders: []Downloaders{ - { - Protocols: []string{"myprotocol", "myprotocols"}, - Command: "echo Download", - }, - }, - } - - if !reflect.DeepEqual(expect, plug.Metadata) { - t.Fatalf("Expected metadata %v, got %v", expect, plug.Metadata) - } -} - -func TestLoadAll(t *testing.T) { - - // Verify that empty dir loads: - if plugs, err := LoadAll("testdata"); err != nil { - t.Fatalf("error loading dir with no plugins: %s", err) - } else if len(plugs) > 0 { - t.Fatalf("expected empty dir to have 0 plugins") - } - - basedir := "testdata/plugdir/good" - plugs, err := LoadAll(basedir) - if err != nil { - t.Fatalf("Could not load %q: %s", basedir, err) - } - - if l := len(plugs); l != 3 { - t.Fatalf("expected 3 plugins, found %d", l) - } - - if plugs[0].Metadata.Name != "downloader" { - t.Errorf("Expected first plugin to be echo, got %q", plugs[0].Metadata.Name) - } - if plugs[1].Metadata.Name != "echo" { - t.Errorf("Expected first plugin to be echo, got %q", plugs[0].Metadata.Name) - } - if plugs[2].Metadata.Name != "hello" { - t.Errorf("Expected second plugin to be hello, got %q", plugs[1].Metadata.Name) - } -} - -func TestFindPlugins(t *testing.T) { - cases := []struct { - name string - plugdirs string - expected int - }{ - { - name: "plugdirs is empty", - plugdirs: "", - expected: 0, - }, - { - name: "plugdirs isn't dir", - plugdirs: "./plugin_test.go", - expected: 0, - }, - { - name: "plugdirs doesn't have plugin", - plugdirs: ".", - expected: 0, - }, - { - name: "normal", - plugdirs: "./testdata/plugdir/good", - expected: 3, - }, - } - for _, c := range cases { - t.Run(t.Name(), func(t *testing.T) { - plugin, _ := FindPlugins(c.plugdirs) - if len(plugin) != c.expected { - t.Errorf("expected: %v, got: %v", c.expected, len(plugin)) - } - }) - } -} - -func TestSetupEnv(t *testing.T) { - name := "pequod" - base := filepath.Join("testdata/helmhome/helm/plugins", name) - - s := cli.New() - s.PluginsDirectory = "testdata/helmhome/helm/plugins" - - SetupPluginEnv(s, name, base) - for _, tt := range []struct { - name, expect string - }{ - {"HELM_PLUGIN_NAME", name}, - {"HELM_PLUGIN_DIR", base}, - } { - if got := os.Getenv(tt.name); got != tt.expect { - t.Errorf("Expected $%s=%q, got %q", tt.name, tt.expect, got) - } - } -} - -func TestSetupEnvWithSpace(t *testing.T) { - name := "sureshdsk" - base := filepath.Join("testdata/helm home/helm/plugins", name) - - s := cli.New() - s.PluginsDirectory = "testdata/helm home/helm/plugins" - - SetupPluginEnv(s, name, base) - for _, tt := range []struct { - name, expect string - }{ - {"HELM_PLUGIN_NAME", name}, - {"HELM_PLUGIN_DIR", base}, - } { - if got := os.Getenv(tt.name); got != tt.expect { - t.Errorf("Expected $%s=%q, got %q", tt.name, tt.expect, got) - } - } -} - -func TestValidatePluginData(t *testing.T) { - // A mock plugin missing any metadata. - mockMissingMeta := &Plugin{ - Dir: "no-such-dir", - } - - for i, item := range []struct { - pass bool - plug *Plugin - }{ - {true, mockPlugin("abcdefghijklmnopqrstuvwxyz0123456789_-ABC")}, - {true, mockPlugin("foo-bar-FOO-BAR_1234")}, - {false, mockPlugin("foo -bar")}, - {false, mockPlugin("$foo -bar")}, // Test leading chars - {false, mockPlugin("foo -bar ")}, // Test trailing chars - {false, mockPlugin("foo\nbar")}, // Test newline - {false, mockMissingMeta}, // Test if the metadata section missing - } { - err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i)) - if item.pass && err != nil { - t.Errorf("failed to validate case %d: %s", i, err) - } else if !item.pass && err == nil { - t.Errorf("expected case %d to fail", i) - } - } -} - -func TestDetectDuplicates(t *testing.T) { - plugs := []*Plugin{ - mockPlugin("foo"), - mockPlugin("bar"), - } - if err := detectDuplicates(plugs); err != nil { - t.Error("no duplicates in the first set") - } - plugs = append(plugs, mockPlugin("foo")) - if err := detectDuplicates(plugs); err == nil { - t.Error("duplicates in the second set") - } -} - -func mockPlugin(name string) *Plugin { - return &Plugin{ - Metadata: &Metadata{ - Name: name, - Version: "v0.1.2", - Usage: "Mock plugin", - Description: "Mock plugin for testing", - Command: "echo mock plugin", - }, - Dir: "no-such-dir", - } -} diff --git a/pkg/helm/pkg/plugin/testdata/plugdir/bad/duplicate-entries/plugin.yaml b/pkg/helm/pkg/plugin/testdata/plugdir/bad/duplicate-entries/plugin.yaml deleted file mode 100644 index 66498be9..00000000 --- a/pkg/helm/pkg/plugin/testdata/plugdir/bad/duplicate-entries/plugin.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "duplicate-entries" -version: "0.1.0" -usage: "usage" -description: |- - description -command: "echo hello" -ignoreFlags: true -hooks: - install: "echo installing..." -hooks: - install: "echo installing something different" diff --git a/pkg/helm/pkg/plugin/testdata/plugdir/good/downloader/plugin.yaml b/pkg/helm/pkg/plugin/testdata/plugdir/good/downloader/plugin.yaml deleted file mode 100644 index c0b90379..00000000 --- a/pkg/helm/pkg/plugin/testdata/plugdir/good/downloader/plugin.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: "downloader" -version: "1.2.3" -usage: "usage" -description: |- - download something -command: "echo Hello" -downloaders: - - protocols: - - "myprotocol" - - "myprotocols" - command: "echo Download" diff --git a/pkg/helm/pkg/plugin/testdata/plugdir/good/echo/plugin.yaml b/pkg/helm/pkg/plugin/testdata/plugdir/good/echo/plugin.yaml deleted file mode 100644 index 8baa35b6..00000000 --- a/pkg/helm/pkg/plugin/testdata/plugdir/good/echo/plugin.yaml +++ /dev/null @@ -1,8 +0,0 @@ -name: "echo" -version: "1.2.3" -usage: "echo something" -description: |- - This is a testing fixture. -command: "echo Hello" -hooks: - install: "echo Installing" diff --git a/pkg/helm/pkg/plugin/testdata/plugdir/good/hello/hello.sh b/pkg/helm/pkg/plugin/testdata/plugdir/good/hello/hello.sh deleted file mode 100755 index dcfd5887..00000000 --- a/pkg/helm/pkg/plugin/testdata/plugdir/good/hello/hello.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -echo "Hello from a Helm plugin" - -echo "PARAMS" -echo $* - -$HELM_BIN ls --all - diff --git a/pkg/helm/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml b/pkg/helm/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml deleted file mode 100644 index b857b55e..00000000 --- a/pkg/helm/pkg/plugin/testdata/plugdir/good/hello/plugin.yaml +++ /dev/null @@ -1,9 +0,0 @@ -name: "hello" -version: "0.1.0" -usage: "usage" -description: |- - description -command: "$HELM_PLUGIN_DIR/hello.sh" -ignoreFlags: true -hooks: - install: "echo installing..." diff --git a/pkg/helm/pkg/postrender/exec.go b/pkg/helm/pkg/postrender/exec.go deleted file mode 100644 index 167e737d..00000000 --- a/pkg/helm/pkg/postrender/exec.go +++ /dev/null @@ -1,109 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package postrender - -import ( - "bytes" - "io" - "os/exec" - "path/filepath" - - "github.com/pkg/errors" -) - -type execRender struct { - binaryPath string - args []string -} - -// NewExec returns a PostRenderer implementation that calls the provided binary. -// It returns an error if the binary cannot be found. If the path does not -// contain any separators, it will search in $PATH, otherwise it will resolve -// any relative paths to a fully qualified path -func NewExec(binaryPath string, args ...string) (PostRenderer, error) { - fullPath, err := getFullPath(binaryPath) - if err != nil { - return nil, err - } - return &execRender{fullPath, args}, nil -} - -// Run the configured binary for the post render -func (p *execRender) Run(renderedManifests *bytes.Buffer) (*bytes.Buffer, error) { - cmd := exec.Command(p.binaryPath, p.args...) - stdin, err := cmd.StdinPipe() - if err != nil { - return nil, err - } - - var postRendered = &bytes.Buffer{} - var stderr = &bytes.Buffer{} - cmd.Stdout = postRendered - cmd.Stderr = stderr - - go func() { - defer stdin.Close() - io.Copy(stdin, renderedManifests) - }() - err = cmd.Run() - if err != nil { - return nil, errors.Wrapf(err, "error while running command %s. error output:\n%s", p.binaryPath, stderr.String()) - } - - return postRendered, nil -} - -// getFullPath returns the full filepath to the binary to execute. If the path -// does not contain any separators, it will search in $PATH, otherwise it will -// resolve any relative paths to a fully qualified path -func getFullPath(binaryPath string) (string, error) { - // NOTE(thomastaylor312): I am leaving this code commented out here. During - // the implementation of post-render, it was brought up that if we are - // relying on plugins, we should actually use the plugin system so it can - // properly handle multiple OSs. This will be a feature add in the future, - // so I left this code for reference. It can be deleted or reused once the - // feature is implemented - - // Manually check the plugin dir first - // if !strings.Contains(binaryPath, string(filepath.Separator)) { - // // First check the plugin dir - // pluginDir := helmpath.DataPath("plugins") // Default location - // // If location for plugins is explicitly set, check there - // if v, ok := os.LookupEnv("HELM_PLUGINS"); ok { - // pluginDir = v - // } - // // The plugins variable can actually contain multiple paths, so loop through those - // for _, p := range filepath.SplitList(pluginDir) { - // _, err := os.Stat(filepath.Join(p, binaryPath)) - // if err != nil && !os.IsNotExist(err) { - // return "", err - // } else if err == nil { - // binaryPath = filepath.Join(p, binaryPath) - // break - // } - // } - // } - - // Now check for the binary using the given path or check if it exists in - // the path and is executable - checkedPath, err := exec.LookPath(binaryPath) - if err != nil { - return "", errors.Wrapf(err, "unable to find binary at %s", binaryPath) - } - - return filepath.Abs(checkedPath) -} diff --git a/pkg/helm/pkg/postrender/exec_test.go b/pkg/helm/pkg/postrender/exec_test.go deleted file mode 100644 index 19a6ec6c..00000000 --- a/pkg/helm/pkg/postrender/exec_test.go +++ /dev/null @@ -1,182 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package postrender - -import ( - "bytes" - "os" - "path/filepath" - "runtime" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const testingScript = `#!/bin/sh -if [ $# -eq 0 ]; then -sed s/FOOTEST/BARTEST/g <&0 -else -sed s/FOOTEST/"$*"/g <&0 -fi -` - -func TestGetFullPath(t *testing.T) { - is := assert.New(t) - t.Run("full path resolves correctly", func(t *testing.T) { - testpath := setupTestingScript(t) - - fullPath, err := getFullPath(testpath) - is.NoError(err) - is.Equal(testpath, fullPath) - }) - - t.Run("relative path resolves correctly", func(t *testing.T) { - testpath := setupTestingScript(t) - - currentDir, err := os.Getwd() - require.NoError(t, err) - relative, err := filepath.Rel(currentDir, testpath) - require.NoError(t, err) - fullPath, err := getFullPath(relative) - is.NoError(err) - is.Equal(testpath, fullPath) - }) - - t.Run("binary in PATH resolves correctly", func(t *testing.T) { - testpath := setupTestingScript(t) - - realPath := os.Getenv("PATH") - os.Setenv("PATH", filepath.Dir(testpath)) - defer func() { - os.Setenv("PATH", realPath) - }() - - fullPath, err := getFullPath(filepath.Base(testpath)) - is.NoError(err) - is.Equal(testpath, fullPath) - }) - - // NOTE(thomastaylor312): See note in getFullPath for more details why this - // is here - - // t.Run("binary in plugin path resolves correctly", func(t *testing.T) { - // testpath, cleanup := setupTestingScript(t) - // defer cleanup() - - // realPath := os.Getenv("HELM_PLUGINS") - // os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)) - // defer func() { - // os.Setenv("HELM_PLUGINS", realPath) - // }() - - // fullPath, err := getFullPath(filepath.Base(testpath)) - // is.NoError(err) - // is.Equal(testpath, fullPath) - // }) - - // t.Run("binary in multiple plugin paths resolves correctly", func(t *testing.T) { - // testpath, cleanup := setupTestingScript(t) - // defer cleanup() - - // realPath := os.Getenv("HELM_PLUGINS") - // os.Setenv("HELM_PLUGINS", filepath.Dir(testpath)+string(os.PathListSeparator)+"/another/dir") - // defer func() { - // os.Setenv("HELM_PLUGINS", realPath) - // }() - - // fullPath, err := getFullPath(filepath.Base(testpath)) - // is.NoError(err) - // is.Equal(testpath, fullPath) - // }) -} - -func TestExecRun(t *testing.T) { - if runtime.GOOS == "windows" { - // the actual Run test uses a basic sed example, so skip this test on windows - t.Skip("skipping on windows") - } - is := assert.New(t) - testpath := setupTestingScript(t) - - renderer, err := NewExec(testpath) - require.NoError(t, err) - - output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) - is.NoError(err) - is.Contains(output.String(), "BARTEST") -} - -func TestNewExecWithOneArgsRun(t *testing.T) { - if runtime.GOOS == "windows" { - // the actual Run test uses a basic sed example, so skip this test on windows - t.Skip("skipping on windows") - } - is := assert.New(t) - testpath := setupTestingScript(t) - - renderer, err := NewExec(testpath, "ARG1") - require.NoError(t, err) - - output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) - is.NoError(err) - is.Contains(output.String(), "ARG1") -} - -func TestNewExecWithTwoArgsRun(t *testing.T) { - if runtime.GOOS == "windows" { - // the actual Run test uses a basic sed example, so skip this test on windows - t.Skip("skipping on windows") - } - is := assert.New(t) - testpath := setupTestingScript(t) - - renderer, err := NewExec(testpath, "ARG1", "ARG2") - require.NoError(t, err) - - output, err := renderer.Run(bytes.NewBufferString("FOOTEST")) - is.NoError(err) - is.Contains(output.String(), "ARG1 ARG2") -} - -func setupTestingScript(t *testing.T) (filepath string) { - t.Helper() - - tempdir := t.TempDir() - - f, err := os.CreateTemp(tempdir, "post-render-test.sh") - if err != nil { - t.Fatalf("unable to create tempfile for testing: %s", err) - } - - _, err = f.WriteString(testingScript) - if err != nil { - t.Fatalf("unable to write tempfile for testing: %s", err) - } - - err = f.Chmod(0755) - if err != nil { - t.Fatalf("unable to make tempfile executable for testing: %s", err) - } - - err = f.Close() - if err != nil { - t.Fatalf("unable to close tempfile after writing: %s", err) - } - - return f.Name() -} diff --git a/pkg/helm/pkg/postrender/postrender.go b/pkg/helm/pkg/postrender/postrender.go deleted file mode 100644 index 3af38429..00000000 --- a/pkg/helm/pkg/postrender/postrender.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package postrender contains an interface that can be implemented for custom -// post-renderers and an exec implementation that can be used for arbitrary -// binaries and scripts -package postrender - -import "bytes" - -type PostRenderer interface { - // Run expects a single buffer filled with Helm rendered manifests. It - // expects the modified results to be returned on a separate buffer or an - // error if there was an issue or failure while running the post render step - Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) -} diff --git a/pkg/helm/pkg/postrenderer/postrenderer.go b/pkg/helm/pkg/postrenderer/postrenderer.go new file mode 100644 index 00000000..3970eaf9 --- /dev/null +++ b/pkg/helm/pkg/postrenderer/postrenderer.go @@ -0,0 +1,35 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package postrenderer + +import ( + "bytes" + "fmt" + + "github.com/werf/nelm/pkg/helm/pkg/cli" +) + +// PostRenderer is an interface different plugin runtimes +// it may be also be used without the factory for custom post-renderers +type PostRenderer interface { + // Run expects a single buffer filled with Helm rendered manifests. It + // expects the modified results to be returned on a separate buffer or an + // error if there was an issue or failure while running the post render step + Run(renderedManifests *bytes.Buffer) (modifiedManifests *bytes.Buffer, err error) +} + +// NewPostRendererPlugin creates a PostRenderer that uses the plugin's Runtime +func NewPostRendererPlugin(_ *cli.EnvSettings, pluginName string, _ ...string) (PostRenderer, error) { + return nil, fmt.Errorf("plugins are not supported, cannot use post-renderer plugin %q", pluginName) +} diff --git a/pkg/helm/pkg/provenance/doc.go b/pkg/helm/pkg/provenance/doc.go index 0c7ae061..f1c19ae2 100644 --- a/pkg/helm/pkg/provenance/doc.go +++ b/pkg/helm/pkg/provenance/doc.go @@ -14,15 +14,15 @@ limitations under the License. */ /* -Package provenance provides tools for establishing the authenticity of a chart. +Package provenance provides tools for establishing the authenticity of packages. In Helm, provenance is established via several factors. The primary factor is the -cryptographic signature of a chart. Chart authors may sign charts, which in turn -provide the necessary metadata to ensure the integrity of the chart file, the -Chart.yaml, and the referenced Docker images. +cryptographic signature of a package. Package authors may sign packages, which in turn +provide the necessary metadata to ensure the integrity of the package file, the +metadata, and the referenced Docker images. A provenance file is clear-signed. This provides cryptographic verification that -a particular block of information (Chart.yaml, archive file, images) have not +a particular block of information (metadata, archive file, images) have not been tampered with or altered. To learn more, read the GnuPG documentation on clear signatures: https://www.gnupg.org/gph/en/manual/x135.html @@ -35,4 +35,4 @@ and using `gpg --verify`, `keybase pgp verify`, or similar: gpg: Signature made Mon Jul 25 17:23:44 2016 MDT using RSA key ID 1FC18762 gpg: Good signature from "Helm Testing (This key should only be used for testing. DO NOT TRUST.) " [ultimate] */ -package provenance // import "helm.sh/helm/v3/pkg/provenance" +package provenance // import "github.com/werf/nelm/pkg/helm/pkg/provenance" diff --git a/pkg/helm/pkg/provenance/sign.go b/pkg/helm/pkg/provenance/sign.go index c3382dbd..57af1ad4 100644 --- a/pkg/helm/pkg/provenance/sign.go +++ b/pkg/helm/pkg/provenance/sign.go @@ -19,20 +19,16 @@ import ( "bytes" "crypto" "encoding/hex" + "errors" + "fmt" "io" "os" - "path/filepath" "strings" - "github.com/pkg/errors" - "golang.org/x/crypto/openpgp" // nolint - "golang.org/x/crypto/openpgp/clearsign" // nolint - "golang.org/x/crypto/openpgp/packet" // nolint + "github.com/ProtonMail/go-crypto/openpgp" //nolint + "github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint + "github.com/ProtonMail/go-crypto/openpgp/packet" //nolint "sigs.k8s.io/yaml" - - hapi "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) var defaultPGPConfig = packet.Config{ @@ -58,7 +54,7 @@ type SumCollection struct { // Verification contains information about a verification operation. type Verification struct { - // SignedBy contains the entity that signed a chart. + // SignedBy contains the entity that signed a package. SignedBy *openpgp.Entity // FileHash is the hash, prepended with the scheme, for the file that was verified. FileHash string @@ -68,11 +64,11 @@ type Verification struct { // Signatory signs things. // -// Signatories can be constructed from a PGP private key file using NewFromFiles +// Signatories can be constructed from a PGP private key file using NewFromFiles, // or they can be constructed manually by setting the Entity to a valid // PGP entity. // -// The same Signatory can be used to sign or validate multiple charts. +// The same Signatory can be used to sign or validate multiple packages. type Signatory struct { // The signatory for this instance of Helm. This is used for signing. Entity *openpgp.Entity @@ -144,7 +140,7 @@ func NewFromKeyring(keyringfile, id string) (*Signatory, error) { } } if vague { - return s, errors.Errorf("more than one key contain the id %q", id) + return s, fmt.Errorf("more than one key contain the id %q", id) } s.Entity = candidate @@ -197,28 +193,20 @@ func (s *Signatory) DecryptKey(fn PassphraseFetcher) error { return s.Entity.PrivateKey.Decrypt(p) } -// ClearSign signs a chart with the given key. -// -// This takes the path to a chart archive file and a key, and it returns a clear signature. +// ClearSign signs package data with the given key and pre-marshalled metadata. // -// The Signatory must have a valid Entity.PrivateKey for this to work. If it does -// not, an error will be returned. -func (s *Signatory) ClearSign(chartpath string, opts helmopts.HelmOptions) (string, error) { +// This is the core signing method that works with data in memory. +// The Signatory must have a valid Entity.PrivateKey for this to work. +func (s *Signatory) ClearSign(archiveData []byte, filename string, metadataBytes []byte) (string, error) { if s.Entity == nil { return "", errors.New("private key not found") } else if s.Entity.PrivateKey == nil { return "", errors.New("provided key is not a private key. Try providing a keyring with secret keys") } - if fi, err := os.Stat(chartpath); err != nil { - return "", err - } else if fi.IsDir() { - return "", errors.New("cannot sign a directory") - } - out := bytes.NewBuffer(nil) - b, err := messageBlock(chartpath, opts) + b, err := messageBlock(archiveData, filename, metadataBytes) if err != nil { return "", err } @@ -237,147 +225,125 @@ func (s *Signatory) ClearSign(chartpath string, opts helmopts.HelmOptions) (stri // In other words, if we call Close here, there's a risk that there's an attempt to use the // private key to sign garbage data (since we know that io.Copy failed, `w` won't contain // anything useful). - return "", errors.Wrap(err, "failed to write to clearsign encoder") + return "", fmt.Errorf("failed to write to clearsign encoder: %w", err) } err = w.Close() if err != nil { - return "", errors.Wrap(err, "failed to either sign or armor message block") + return "", fmt.Errorf("failed to either sign or armor message block: %w", err) } return out.String(), nil } -// Verify checks a signature and verifies that it is legit for a chart. -func (s *Signatory) Verify(chartpath, sigpath string) (*Verification, error) { +// Verify checks a signature and verifies that it is legit for package data. +// This is the core verification method that works with data in memory. +func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Verification, error) { ver := &Verification{} - for _, fname := range []string{chartpath, sigpath} { - if fi, err := os.Stat(fname); err != nil { - return ver, err - } else if fi.IsDir() { - return ver, errors.Errorf("%s cannot be a directory", fname) - } - } // First verify the signature - sig, err := s.decodeSignature(sigpath) - if err != nil { - return ver, errors.Wrap(err, "failed to decode signature") + block, _ := clearsign.Decode(provData) + if block == nil { + return ver, errors.New("signature block not found") } - by, err := s.verifySignature(sig) + by, err := s.verifySignature(block) if err != nil { return ver, err } ver.SignedBy = by - // Second, verify the hash of the tarball. - sum, err := DigestFile(chartpath) + // Second, verify the hash of the data. + sum, err := Digest(bytes.NewBuffer(archiveData)) if err != nil { return ver, err } - _, sums, err := parseMessageBlock(sig.Plaintext) + sums, err := parseMessageBlock(block.Plaintext) if err != nil { return ver, err } sum = "sha256:" + sum - basename := filepath.Base(chartpath) - if sha, ok := sums.Files[basename]; !ok { - return ver, errors.Errorf("provenance does not contain a SHA for a file named %q", basename) + if sha, ok := sums.Files[filename]; !ok { + return ver, fmt.Errorf("provenance does not contain a SHA for a file named %q", filename) } else if sha != sum { - return ver, errors.Errorf("sha256 sum does not match for %s: %q != %q", basename, sha, sum) + return ver, fmt.Errorf("sha256 sum does not match for %s: %q != %q", filename, sha, sum) } ver.FileHash = sum - ver.FileName = basename + ver.FileName = filename // TODO: when image signing is added, verify that here. return ver, nil } -func (s *Signatory) decodeSignature(filename string) (*clearsign.Block, error) { - data, err := os.ReadFile(filename) - if err != nil { - return nil, err - } - - block, _ := clearsign.Decode(data) - if block == nil { - // There was no sig in the file. - return nil, errors.New("signature block not found") - } - - return block, nil -} - // verifySignature verifies that the given block is validly signed, and returns the signer. func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { return openpgp.CheckDetachedSignature( s.KeyRing, - bytes.NewBuffer(block.Bytes), + bytes.NewReader(block.Bytes), block.ArmoredSignature.Body, + &defaultPGPConfig, ) } -func messageBlock(chartpath string, opts helmopts.HelmOptions) (*bytes.Buffer, error) { - var b *bytes.Buffer - // Checksum the archive - chash, err := DigestFile(chartpath) +// messageBlock creates a message block from archive data and pre-marshalled metadata +func messageBlock(archiveData []byte, filename string, metadataBytes []byte) (*bytes.Buffer, error) { + // Checksum the archive data + chash, err := Digest(bytes.NewBuffer(archiveData)) if err != nil { - return b, err + return nil, err } - base := filepath.Base(chartpath) sums := &SumCollection{ Files: map[string]string{ - base: "sha256:" + chash, + filename: "sha256:" + chash, }, } - // Load the archive into memory. - chart, err := loader.LoadFile(chartpath, opts) - if err != nil { - return b, err - } - - // Buffer a hash + checksums YAML file - data, err := yaml.Marshal(chart.Metadata) - if err != nil { - return b, err - } - + // Buffer the metadata + checksums YAML file // FIXME: YAML uses ---\n as a file start indicator, but this is not legal in a PGP // clearsign block. So we use ...\n, which is the YAML document end marker. // http://yaml.org/spec/1.2/spec.html#id2800168 - b = bytes.NewBuffer(data) + b := bytes.NewBuffer(metadataBytes) b.WriteString("\n...\n") - data, err = yaml.Marshal(sums) + data, err := yaml.Marshal(sums) if err != nil { - return b, err + return nil, err } b.Write(data) return b, nil } -// parseMessageBlock -func parseMessageBlock(data []byte) (*hapi.Metadata, *SumCollection, error) { - // This sucks. +// parseMessageBlock parses a message block and returns only checksums (metadata ignored like upstream) +func parseMessageBlock(data []byte) (*SumCollection, error) { + sc := &SumCollection{} + + // We ignore metadata, just like upstream - only need checksums for verification + if err := ParseMessageBlock(data, nil, sc); err != nil { + return sc, err + } + return sc, nil +} + +// ParseMessageBlock parses a message block containing metadata and checksums. +// +// This is the generic version that can work with any metadata type. +// The metadata parameter should be a pointer to a struct that can be unmarshaled from YAML. +func ParseMessageBlock(data []byte, metadata interface{}, sums *SumCollection) error { parts := bytes.Split(data, []byte("\n...\n")) if len(parts) < 2 { - return nil, nil, errors.New("message block must have at least two parts") + return errors.New("message block must have at least two parts") } - md := &hapi.Metadata{} - sc := &SumCollection{} - - if err := yaml.Unmarshal(parts[0], md); err != nil { - return md, sc, err + if metadata != nil { + if err := yaml.Unmarshal(parts[0], metadata); err != nil { + return err + } } - err := yaml.Unmarshal(parts[1], sc) - return md, sc, err + return yaml.Unmarshal(parts[1], sums) } // loadKey loads a GPG key found at a particular path. @@ -406,7 +372,7 @@ func loadKeyRing(ringpath string) (openpgp.EntityList, error) { // It takes the path to the archive file, and returns a string representation of // the SHA256 sum. // -// The intended use of this function is to generate a sum of a chart TGZ file. +// This function can be used to generate a sum of any package archive file. func DigestFile(filename string) (string, error) { f, err := os.Open(filename) if err != nil { diff --git a/pkg/helm/pkg/provenance/sign_test.go b/pkg/helm/pkg/provenance/sign_test.go index 17f727ea..2fbf3b88 100644 --- a/pkg/helm/pkg/provenance/sign_test.go +++ b/pkg/helm/pkg/provenance/sign_test.go @@ -16,6 +16,7 @@ limitations under the License. package provenance import ( + "context" "crypto" "fmt" "io" @@ -24,7 +25,13 @@ import ( "strings" "testing" - pgperrors "golang.org/x/crypto/openpgp/errors" //nolint + pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" //nolint + "github.com/ProtonMail/go-crypto/openpgp/packet" //nolint + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" ) const ( @@ -34,7 +41,7 @@ const ( // phrase. Use `gpg --export-secret-keys helm-test` to export the secret. testKeyfile = "testdata/helm-test-key.secret" - // testPasswordKeyFile is a keyfile with a password. + // testPasswordKeyfile is a keyfile with a password. testPasswordKeyfile = "testdata/helm-password-key.secret" // testPubfile is the public key file. @@ -56,6 +63,9 @@ const ( // testTamperedSigBlock is a tampered copy of msgblock.yaml.asc testTamperedSigBlock = "testdata/msgblock.yaml.tampered" + // testMixedKeyring points to a keyring containing RSA and ed25519 keys. + testMixedKeyring = "testdata/helm-mixed-keyring.pub" + // testSumfile points to a SHA256 sum generated by an external tool. // We always want to validate against an external tool's representation to // verify that we haven't done something stupid. This file was generated @@ -75,8 +85,33 @@ files: hashtest-1.2.3.tgz: sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888 ` +// loadChartMetadataForSigning is a test helper that loads chart metadata and marshals it to YAML bytes +func loadChartMetadataForSigning(t *testing.T, chartPath string) []byte { + t.Helper() + + chart, err := loader.LoadFile(context.Background(), chartPath) + if err != nil { + t.Fatal(err) + } + + metadataBytes, err := yaml.Marshal(chart.Metadata) + if err != nil { + t.Fatal(err) + } + + return metadataBytes +} + func TestMessageBlock(t *testing.T) { - out, err := messageBlock(testChartfile) + metadataBytes := loadChartMetadataForSigning(t, testChartfile) + + // Read the chart file data + archiveData, err := os.ReadFile(testChartfile) + if err != nil { + t.Fatal(err) + } + + out, err := messageBlock(archiveData, filepath.Base(testChartfile), metadataBytes) if err != nil { t.Fatal(err) } @@ -88,14 +123,12 @@ func TestMessageBlock(t *testing.T) { } func TestParseMessageBlock(t *testing.T) { - md, sc, err := parseMessageBlock([]byte(testMessageBlock)) + sc, err := parseMessageBlock([]byte(testMessageBlock)) if err != nil { t.Fatal(err) } - if md.Name != "hashtest" { - t.Errorf("Expected name %q, got %q", "hashtest", md.Name) - } + // parseMessageBlock only returns checksums, not metadata (like upstream) if lsc := len(sc.Files); lsc != 1 { t.Errorf("Expected 1 file, got %d", lsc) @@ -196,7 +229,7 @@ func TestDecryptKey(t *testing.T) { } // We give this a simple callback that returns the password. - if err := k.DecryptKey(func(s string) ([]byte, error) { + if err := k.DecryptKey(func(_ string) ([]byte, error) { return []byte("secret"), nil }); err != nil { t.Fatal(err) @@ -208,7 +241,7 @@ func TestDecryptKey(t *testing.T) { t.Fatal(err) } // Now we give it a bogus password. - if err := k.DecryptKey(func(s string) ([]byte, error) { + if err := k.DecryptKey(func(_ string) ([]byte, error) { return []byte("secrets_and_lies"), nil }); err == nil { t.Fatal("Expected an error when giving a bogus passphrase") @@ -221,7 +254,15 @@ func TestClearSign(t *testing.T) { t.Fatal(err) } - sig, err := signer.ClearSign(testChartfile) + metadataBytes := loadChartMetadataForSigning(t, testChartfile) + + // Read the chart file data + archiveData, err := os.ReadFile(testChartfile) + if err != nil { + t.Fatal(err) + } + + sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes) if err != nil { t.Fatal(err) } @@ -232,6 +273,56 @@ func TestClearSign(t *testing.T) { } } +func TestMixedKeyringRSASigningAndVerification(t *testing.T) { + signer, err := NewFromFiles(testKeyfile, testMixedKeyring) + require.NoError(t, err) + + require.NotEmpty(t, signer.KeyRing, "expected signer keyring to be loaded") + + hasEdDSA := false + for _, entity := range signer.KeyRing { + if entity.PrimaryKey != nil && entity.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA { + hasEdDSA = true + break + } + + for _, subkey := range entity.Subkeys { + if subkey.PublicKey != nil && subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA { + hasEdDSA = true + break + } + } + + if hasEdDSA { + break + } + } + + assert.True(t, hasEdDSA, "expected %s to include an Ed25519 public key", testMixedKeyring) + + require.NotNil(t, signer.Entity, "expected signer entity to be loaded") + require.NotNil(t, signer.Entity.PrivateKey, "expected signer private key to be loaded") + assert.Equal(t, packet.PubKeyAlgoRSA, signer.Entity.PrivateKey.PubKeyAlgo, "expected RSA key") + + metadataBytes := loadChartMetadataForSigning(t, testChartfile) + + archiveData, err := os.ReadFile(testChartfile) + require.NoError(t, err) + + sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes) + require.NoError(t, err, "failed to sign chart") + + verification, err := signer.Verify(archiveData, []byte(sig), filepath.Base(testChartfile)) + require.NoError(t, err, "failed to verify chart signature") + + require.NotNil(t, verification.SignedBy, "expected verification to include signer") + require.NotNil(t, verification.SignedBy.PrimaryKey, "expected verification to include signer primary key") + assert.Equal(t, packet.PubKeyAlgoRSA, verification.SignedBy.PrimaryKey.PubKeyAlgo, "expected verification to report RSA key") + + _, ok := verification.SignedBy.Identities[testKeyName] + assert.True(t, ok, "expected verification to be signed by %q", testKeyName) +} + // failSigner always fails to sign and returns an error type failSigner struct{} @@ -252,7 +343,15 @@ func TestClearSignError(t *testing.T) { // ensure that signing always fails signer.Entity.PrivateKey.PrivateKey = failSigner{} - sig, err := signer.ClearSign(testChartfile) + metadataBytes := loadChartMetadataForSigning(t, testChartfile) + + // Read the chart file data + archiveData, err := os.ReadFile(testChartfile) + if err != nil { + t.Fatal(err) + } + + sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes) if err == nil { t.Fatal("didn't get an error from ClearSign but expected one") } @@ -262,54 +361,25 @@ func TestClearSignError(t *testing.T) { } } -func TestDecodeSignature(t *testing.T) { - // Unlike other tests, this does a round-trip test, ensuring that a signature - // generated by the library can also be verified by the library. - +func TestVerify(t *testing.T) { signer, err := NewFromFiles(testKeyfile, testPubfile) if err != nil { t.Fatal(err) } - sig, err := signer.ClearSign(testChartfile) + // Read the chart file data + archiveData, err := os.ReadFile(testChartfile) if err != nil { t.Fatal(err) } - f, err := os.CreateTemp("", "helm-test-sig-") - if err != nil { - t.Fatal(err) - } - - tname := f.Name() - defer func() { - os.Remove(tname) - }() - f.WriteString(sig) - f.Close() - - sig2, err := signer.decodeSignature(tname) + // Read the signature file data + sigData, err := os.ReadFile(testSigBlock) if err != nil { t.Fatal(err) } - by, err := signer.verifySignature(sig2) - if err != nil { - t.Fatal(err) - } - - if _, ok := by.Identities[testKeyName]; !ok { - t.Errorf("Expected identity %q", testKeyName) - } -} - -func TestVerify(t *testing.T) { - signer, err := NewFromFiles(testKeyfile, testPubfile) - if err != nil { - t.Fatal(err) - } - - if ver, err := signer.Verify(testChartfile, testSigBlock); err != nil { + if ver, err := signer.Verify(archiveData, sigData, filepath.Base(testChartfile)); err != nil { t.Errorf("Failed to pass verify. Err: %s", err) } else if len(ver.FileHash) == 0 { t.Error("Verification is missing hash.") @@ -319,7 +389,13 @@ func TestVerify(t *testing.T) { t.Errorf("FileName is unexpectedly %q", ver.FileName) } - if _, err = signer.Verify(testChartfile, testTamperedSigBlock); err == nil { + // Read the tampered signature file data + tamperedSigData, err := os.ReadFile(testTamperedSigBlock) + if err != nil { + t.Fatal(err) + } + + if _, err = signer.Verify(archiveData, tamperedSigData, filepath.Base(testChartfile)); err == nil { t.Errorf("Expected %s to fail.", testTamperedSigBlock) } diff --git a/pkg/helm/pkg/provenance/testdata/helm-mixed-keyring.pub b/pkg/helm/pkg/provenance/testdata/helm-mixed-keyring.pub new file mode 100644 index 00000000..7985bd20 Binary files /dev/null and b/pkg/helm/pkg/provenance/testdata/helm-mixed-keyring.pub differ diff --git a/pkg/helm/pkg/pusher/ocipusher.go b/pkg/helm/pkg/pusher/ocipusher.go index 5056cba8..67074026 100644 --- a/pkg/helm/pkg/pusher/ocipusher.go +++ b/pkg/helm/pkg/pusher/ocipusher.go @@ -16,7 +16,10 @@ limitations under the License. package pusher import ( + "context" + "errors" "fmt" + "io/fs" "net" "net/http" "os" @@ -24,12 +27,9 @@ import ( "strings" "time" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/intern/tlsutil" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // OCIPusher is the default OCI backend handler @@ -38,18 +38,18 @@ type OCIPusher struct { } // Push performs a Push from repo.Pusher. -func (pusher *OCIPusher) Push(chartRef, href string, opts helmopts.HelmOptions, options ...Option) error { +func (pusher *OCIPusher) Push(chartRef, href string, options ...Option) error { for _, opt := range options { opt(&pusher.opts) } - return pusher.push(chartRef, href, opts) + return pusher.push(chartRef, href) } -func (pusher *OCIPusher) push(chartRef, href string, opts helmopts.HelmOptions) error { +func (pusher *OCIPusher) push(chartRef, href string) error { stat, err := os.Stat(chartRef) if err != nil { - if os.IsNotExist(err) { - return errors.Errorf("%s: no such file", chartRef) + if errors.Is(err, fs.ErrNotExist) { + return fmt.Errorf("%s: no such file", chartRef) } return err } @@ -57,7 +57,7 @@ func (pusher *OCIPusher) push(chartRef, href string, opts helmopts.HelmOptions) return errors.New("cannot push directory, must provide chart archive (.tgz)") } - meta, err := loader.Load(chartRef, opts) + meta, err := loader.Load(context.Background(), chartRef) if err != nil { return err } @@ -90,7 +90,11 @@ func (pusher *OCIPusher) push(chartRef, href string, opts helmopts.HelmOptions) path.Join(strings.TrimPrefix(href, fmt.Sprintf("%s://", registry.OCIScheme)), meta.Metadata.Name), meta.Metadata.Version) - _, err = client.Push(chartBytes, ref, opts, pushOpts...) + // The time the chart was "created" is semantically the time the chart archive file was last written(modified) + chartArchiveFileCreatedTime := stat.ModTime() + pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339))) + + _, err = client.Push(chartBytes, ref, pushOpts...) return err } @@ -106,10 +110,14 @@ func NewOCIPusher(ops ...Option) (Pusher, error) { } func (pusher *OCIPusher) newRegistryClient() (*registry.Client, error) { - if (pusher.opts.certFile != "" && pusher.opts.keyFile != "") || pusher.opts.caFile != "" || pusher.opts.insecureSkipTLSverify { - tlsConf, err := tlsutil.NewClientTLS(pusher.opts.certFile, pusher.opts.keyFile, pusher.opts.caFile, pusher.opts.insecureSkipTLSverify) + if (pusher.opts.certFile != "" && pusher.opts.keyFile != "") || pusher.opts.caFile != "" || pusher.opts.insecureSkipTLSVerify { + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(pusher.opts.insecureSkipTLSVerify), + tlsutil.WithCertKeyPairFiles(pusher.opts.certFile, pusher.opts.keyFile), + tlsutil.WithCAFile(pusher.opts.caFile), + ) if err != nil { - return nil, errors.Wrap(err, "can't create TLS config for client") + return nil, fmt.Errorf("can't create TLS config for client: %w", err) } registryClient, err := registry.NewClient( diff --git a/pkg/helm/pkg/pusher/ocipusher_test.go b/pkg/helm/pkg/pusher/ocipusher_test.go index 1cd7a6f9..9151cb3d 100644 --- a/pkg/helm/pkg/pusher/ocipusher_test.go +++ b/pkg/helm/pkg/pusher/ocipusher_test.go @@ -1,3 +1,5 @@ +//go:build !windows + /* Copyright The Helm Authors. Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,7 +18,10 @@ limitations under the License. package pusher import ( + "io" + "os" "path/filepath" + "strings" "testing" "github.com/werf/nelm/pkg/helm/pkg/registry" @@ -35,13 +40,13 @@ func TestNewOCIPusher(t *testing.T) { cd := "../../testdata" join := filepath.Join ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") - insecureSkipTLSverify := false + insecureSkipTLSVerify := false plainHTTP := false // Test with options p, err = NewOCIPusher( WithTLSClientConfig(pub, priv, ca), - WithInsecureSkipTLSVerify(insecureSkipTLSverify), + WithInsecureSkipTLSVerify(insecureSkipTLSVerify), WithPlainHTTP(plainHTTP), ) if err != nil { @@ -69,8 +74,8 @@ func TestNewOCIPusher(t *testing.T) { t.Errorf("Expected NewOCIPusher to have plainHTTP as %t, got %t", plainHTTP, op.opts.plainHTTP) } - if op.opts.insecureSkipTLSverify != insecureSkipTLSverify { - t.Errorf("Expected NewOCIPusher to have insecureSkipVerifyTLS as %t, got %t", insecureSkipTLSverify, op.opts.insecureSkipTLSverify) + if op.opts.insecureSkipTLSVerify != insecureSkipTLSVerify { + t.Errorf("Expected NewOCIPusher to have insecureSkipVerifyTLS as %t, got %t", insecureSkipTLSVerify, op.opts.insecureSkipTLSVerify) } // Test if setting registryClient is being passed to the ops @@ -94,3 +99,330 @@ func TestNewOCIPusher(t *testing.T) { t.Errorf("Expected NewOCIPusher to contain %p as RegistryClient, got %p", registryClient, op.opts.registryClient) } } + +func TestOCIPusher_Push_ErrorHandling(t *testing.T) { + tests := []struct { + name string + chartRef string + expectedError string + setupFunc func() string + }{ + { + name: "non-existent file", + chartRef: "/non/existent/file.tgz", + expectedError: "no such file", + }, + { + name: "directory instead of file", + expectedError: "cannot push directory, must provide chart archive (.tgz)", + setupFunc: func() string { + tempDir := t.TempDir() + return tempDir + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pusher, err := NewOCIPusher() + if err != nil { + t.Fatal(err) + } + + chartRef := tt.chartRef + if tt.setupFunc != nil { + chartRef = tt.setupFunc() + } + + err = pusher.Push(chartRef, "oci://localhost:5000/test") + if err == nil { + t.Fatal("Expected error but got none") + } + + if !strings.Contains(err.Error(), tt.expectedError) { + t.Errorf("Expected error containing %q, got %q", tt.expectedError, err.Error()) + } + }) + } +} + +func TestOCIPusher_newRegistryClient(t *testing.T) { + cd := "../../testdata" + join := filepath.Join + ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") + + tests := []struct { + name string + opts []Option + expectError bool + errorContains string + }{ + { + name: "plain HTTP", + opts: []Option{WithPlainHTTP(true)}, + }, + { + name: "with TLS client config", + opts: []Option{ + WithTLSClientConfig(pub, priv, ca), + }, + }, + { + name: "with insecure skip TLS verify", + opts: []Option{ + WithInsecureSkipTLSVerify(true), + }, + }, + { + name: "with cert and key only", + opts: []Option{ + WithTLSClientConfig(pub, priv, ""), + }, + }, + { + name: "with CA file only", + opts: []Option{ + WithTLSClientConfig("", "", ca), + }, + }, + { + name: "default client without options", + opts: []Option{}, + }, + { + name: "invalid cert file", + opts: []Option{ + WithTLSClientConfig("/non/existent/cert.pem", priv, ca), + }, + expectError: true, + errorContains: "can't create TLS config", + }, + { + name: "invalid key file", + opts: []Option{ + WithTLSClientConfig(pub, "/non/existent/key.pem", ca), + }, + expectError: true, + errorContains: "can't create TLS config", + }, + { + name: "invalid CA file", + opts: []Option{ + WithTLSClientConfig("", "", "/non/existent/ca.crt"), + }, + expectError: true, + errorContains: "can't create TLS config", + }, + { + name: "combined TLS options", + opts: []Option{ + WithTLSClientConfig(pub, priv, ca), + WithInsecureSkipTLSVerify(true), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pusher, err := NewOCIPusher(tt.opts...) + if err != nil { + t.Fatal(err) + } + + op, ok := pusher.(*OCIPusher) + if !ok { + t.Fatal("Expected *OCIPusher") + } + + client, err := op.newRegistryClient() + if tt.expectError { + if err == nil { + t.Fatal("Expected error but got none") + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error containing %q, got %q", tt.errorContains, err.Error()) + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if client == nil { + t.Fatal("Expected non-nil registry client") + } + } + }) + } +} + +func TestOCIPusher_Push_ChartOperations(t *testing.T) { + // Path to test charts + chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz" + chartWithProvPath := "../../pkg/cmd/testdata/testcharts/signtest-0.1.0.tgz" + + tests := []struct { + name string + chartRef string + href string + options []Option + setupFunc func(t *testing.T) (string, func()) + expectError bool + errorContains string + }{ + { + name: "invalid chart file", + chartRef: "../../pkg/action/testdata/charts/corrupted-compressed-chart.tgz", + href: "oci://localhost:5000/test", + expectError: true, + errorContains: "does not appear to be a gzipped archive", + }, + { + name: "chart read error", + setupFunc: func(t *testing.T) (string, func()) { + t.Helper() + // Create a valid chart file that we'll make unreadable + tempDir := t.TempDir() + tempChart := filepath.Join(tempDir, "temp-chart.tgz") + + // Copy a valid chart + src, err := os.Open(chartPath) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(tempChart) + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(dst, src); err != nil { + t.Fatal(err) + } + dst.Close() + + // Make the file unreadable + if err := os.Chmod(tempChart, 0000); err != nil { + t.Fatal(err) + } + + return tempChart, func() { + os.Chmod(tempChart, 0644) // Restore permissions for cleanup + } + }, + href: "oci://localhost:5000/test", + expectError: true, + errorContains: "permission denied", + }, + { + name: "push with provenance file - loading phase", + chartRef: chartWithProvPath, + href: "oci://registry.example.com/charts", + setupFunc: func(t *testing.T) (string, func()) { + t.Helper() + // Copy chart and create a .prov file for it + tempDir := t.TempDir() + tempChart := filepath.Join(tempDir, "signtest-0.1.0.tgz") + tempProv := filepath.Join(tempDir, "signtest-0.1.0.tgz.prov") + + // Copy chart file + src, err := os.Open(chartWithProvPath) + if err != nil { + t.Fatal(err) + } + defer src.Close() + + dst, err := os.Create(tempChart) + if err != nil { + t.Fatal(err) + } + + if _, err := io.Copy(dst, src); err != nil { + t.Fatal(err) + } + dst.Close() + + // Create provenance file + if err := os.WriteFile(tempProv, []byte("test provenance data"), 0644); err != nil { + t.Fatal(err) + } + + return tempChart, func() {} + }, + expectError: true, // Will fail at the registry push step + errorContains: "", // Error depends on registry client behavior + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chartRef := tt.chartRef + var cleanup func() + + if tt.setupFunc != nil { + chartRef, cleanup = tt.setupFunc(t) + if cleanup != nil { + defer cleanup() + } + } + + // Skip test if chart file doesn't exist and we're not expecting an error + if _, err := os.Stat(chartRef); err != nil && !tt.expectError { + t.Skipf("Test chart %s not found, skipping test", chartRef) + } + + pusher, err := NewOCIPusher(tt.options...) + if err != nil { + t.Fatal(err) + } + + err = pusher.Push(chartRef, tt.href) + + if tt.expectError { + if err == nil { + t.Fatal("Expected error but got none") + } + if tt.errorContains != "" && !strings.Contains(err.Error(), tt.errorContains) { + t.Errorf("Expected error containing %q, got %q", tt.errorContains, err.Error()) + } + } else { + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + } + }) + } +} + +func TestOCIPusher_Push_MultipleOptions(t *testing.T) { + chartPath := "../../pkg/cmd/testdata/testcharts/compressedchart-0.1.0.tgz" + + // Skip test if chart file doesn't exist + if _, err := os.Stat(chartPath); err != nil { + t.Skipf("Test chart %s not found, skipping test", chartPath) + } + + pusher, err := NewOCIPusher() + if err != nil { + t.Fatal(err) + } + + // Test that multiple options are applied correctly + err = pusher.Push(chartPath, "oci://localhost:5000/test", + WithPlainHTTP(true), + WithInsecureSkipTLSVerify(true), + ) + + // We expect an error since we're not actually pushing to a registry + if err == nil { + t.Fatal("Expected error when pushing without a valid registry") + } + + // Verify options were applied + op := pusher.(*OCIPusher) + if !op.opts.plainHTTP { + t.Error("Expected plainHTTP option to be applied") + } + if !op.opts.insecureSkipTLSVerify { + t.Error("Expected insecureSkipTLSVerify option to be applied") + } +} diff --git a/pkg/helm/pkg/pusher/pusher.go b/pkg/helm/pkg/pusher/pusher.go index d623f5a1..168ee93d 100644 --- a/pkg/helm/pkg/pusher/pusher.go +++ b/pkg/helm/pkg/pusher/pusher.go @@ -17,11 +17,11 @@ limitations under the License. package pusher import ( - "github.com/pkg/errors" + "fmt" + "slices" "github.com/werf/nelm/pkg/helm/pkg/cli" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // options are generic parameters to be provided to the pusher during instantiation. @@ -32,7 +32,7 @@ type options struct { certFile string keyFile string caFile string - insecureSkipTLSverify bool + insecureSkipTLSVerify bool plainHTTP bool } @@ -59,7 +59,7 @@ func WithTLSClientConfig(certFile, keyFile, caFile string) Option { // WithInsecureSkipTLSVerify determines if a TLS Certificate will be checked func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) Option { return func(opts *options) { - opts.insecureSkipTLSverify = insecureSkipTLSVerify + opts.insecureSkipTLSVerify = insecureSkipTLSVerify } } @@ -72,7 +72,7 @@ func WithPlainHTTP(plainHTTP bool) Option { // Pusher is an interface to support upload to the specified URL. type Pusher interface { // Push file content by url string - Push(chartRef, url string, opts helmopts.HelmOptions, options ...Option) error + Push(chartRef, url string, options ...Option) error } // Constructor is the function for every pusher which creates a specific instance @@ -87,12 +87,7 @@ type Provider struct { // Provides returns true if the given scheme is supported by this Provider. func (p Provider) Provides(scheme string) bool { - for _, i := range p.Schemes { - if i == scheme { - return true - } - } - return false + return slices.Contains(p.Schemes, scheme) } // Providers is a collection of Provider objects. @@ -107,7 +102,7 @@ func (p Providers) ByScheme(scheme string) (Pusher, error) { return pp.New() } } - return nil, errors.Errorf("scheme %q not supported", scheme) + return nil, fmt.Errorf("scheme %q not supported", scheme) } var ociProvider = Provider{ diff --git a/pkg/helm/pkg/registry/chart.go b/pkg/helm/pkg/registry/chart.go new file mode 100644 index 00000000..54010a06 --- /dev/null +++ b/pkg/helm/pkg/registry/chart.go @@ -0,0 +1,125 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry // import "github.com/werf/nelm/pkg/helm/pkg/registry" + +import ( + "bytes" + "context" + "strings" + "time" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +var immutableOciAnnotations = []string{ + ocispec.AnnotationVersion, + ocispec.AnnotationTitle, +} + +// extractChartMeta is used to extract a chart metadata from a byte array +func extractChartMeta(chartData []byte) (*chart.Metadata, error) { + ch, err := loader.LoadArchive(context.Background(), bytes.NewReader(chartData)) + if err != nil { + return nil, err + } + return ch.Metadata, nil +} + +// generateOCIAnnotations will generate OCI annotations to include within the OCI manifest +func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { + + // Get annotations from Chart attributes + ociAnnotations := generateChartOCIAnnotations(meta, creationTime) + + // Copy Chart annotations +annotations: + for chartAnnotationKey, chartAnnotationValue := range meta.Annotations { + + // Avoid overriding key properties + for _, immutableOciKey := range immutableOciAnnotations { + if immutableOciKey == chartAnnotationKey { + continue annotations + } + } + + // Add chart annotation + ociAnnotations[chartAnnotationKey] = chartAnnotationValue + } + + return ociAnnotations +} + +// generateChartOCIAnnotations will generate OCI annotations from the provided chart +func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { + chartOCIAnnotations := map[string]string{} + + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version) + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) + + if len(creationTime) == 0 { + creationTime = time.Now().UTC().Format(time.RFC3339) + } + + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime) + + if len(meta.Sources) > 0 { + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0]) + } + + if len(meta.Maintainers) > 0 { + var maintainerSb strings.Builder + + for maintainerIdx, maintainer := range meta.Maintainers { + + if len(maintainer.Name) > 0 { + maintainerSb.WriteString(maintainer.Name) + } + + if len(maintainer.Email) > 0 { + maintainerSb.WriteString(" (") + maintainerSb.WriteString(maintainer.Email) + maintainerSb.WriteString(")") + } + + if maintainerIdx < len(meta.Maintainers)-1 { + maintainerSb.WriteString(", ") + } + + } + + chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String()) + + } + + return chartOCIAnnotations +} + +// addToMap takes an existing map and adds an item if the value is not empty +func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string { + + // Add item to map if its + if len(strings.TrimSpace(newValue)) > 0 { + inputMap[newKey] = newValue + } + + return inputMap +} diff --git a/pkg/helm/pkg/registry/chart_test.go b/pkg/helm/pkg/registry/chart_test.go new file mode 100644 index 00000000..14aa3810 --- /dev/null +++ b/pkg/helm/pkg/registry/chart_test.go @@ -0,0 +1,274 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry // import "github.com/werf/nelm/pkg/helm/pkg/registry" + +import ( + "reflect" + "testing" + "time" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" +) + +func TestGenerateOCIChartAnnotations(t *testing.T) { + + nowString := time.Now().Format(time.RFC3339) + + tests := []struct { + name string + chart *chart.Metadata + expect map[string]string + }{ + { + "Baseline chart", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + }, + }, + { + "Simple chart values", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + }, + }, + { + "Maintainer without email", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + Maintainers: []*chart.Maintainer{ + { + Name: "John Snow", + }, + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + "org.opencontainers.image.authors": "John Snow", + }, + }, + { + "Maintainer with email", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + Maintainers: []*chart.Maintainer{ + {Name: "John Snow", Email: "john@winterfell.com"}, + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + "org.opencontainers.image.authors": "John Snow (john@winterfell.com)", + }, + }, + { + "Multiple Maintainers", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Home: "https://helm.sh", + Maintainers: []*chart.Maintainer{ + {Name: "John Snow", Email: "john@winterfell.com"}, + {Name: "Jane Snow"}, + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.url": "https://helm.sh", + "org.opencontainers.image.authors": "John Snow (john@winterfell.com), Jane Snow", + }, + }, + { + "Chart with Sources", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Sources: []string{ + "https://github.com/helm/helm", + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.source": "https://github.com/helm/helm", + }, + }, + } + + for _, tt := range tests { + + result := generateChartOCIAnnotations(tt.chart, nowString) + + if !reflect.DeepEqual(tt.expect, result) { + t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) + } + + } +} + +func TestGenerateOCIAnnotations(t *testing.T) { + + nowString := time.Now().Format(time.RFC3339) + + tests := []struct { + name string + chart *chart.Metadata + expect map[string]string + }{ + { + "Baseline chart", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.created": nowString, + }, + }, + { + "Simple chart values with custom Annotations", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Annotations: map[string]string{ + "extrakey": "extravlue", + "anotherkey": "anothervalue", + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.created": nowString, + "extrakey": "extravlue", + "anotherkey": "anothervalue", + }, + }, + { + "Verify Chart Name and Version cannot be overridden from annotations", + &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + Description: "OCI Helm Chart", + Annotations: map[string]string{ + "org.opencontainers.image.title": "badchartname", + "org.opencontainers.image.version": "1.0.0", + "extrakey": "extravlue", + }, + }, + map[string]string{ + "org.opencontainers.image.title": "oci", + "org.opencontainers.image.version": "0.0.1", + "org.opencontainers.image.description": "OCI Helm Chart", + "org.opencontainers.image.created": nowString, + "extrakey": "extravlue", + }, + }, + } + + for _, tt := range tests { + + result := generateOCIAnnotations(tt.chart, nowString) + + if !reflect.DeepEqual(tt.expect, result) { + t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) + } + + } +} + +func TestGenerateOCICreatedAnnotations(t *testing.T) { + + nowTime := time.Now() + nowTimeString := nowTime.Format(time.RFC3339) + + testChart := &chart.Metadata{ + Name: "oci", + Version: "0.0.1", + } + + result := generateOCIAnnotations(testChart, nowTimeString) + + // Check that created annotation exists + if _, ok := result[ocispec.AnnotationCreated]; !ok { + t.Errorf("%s annotation not created", ocispec.AnnotationCreated) + } + + // Verify value of created artifact in RFC3339 format + if _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) + } + + // Verify default creation time set + result = generateOCIAnnotations(testChart, "") + + // Check that created annotation exists + if _, ok := result[ocispec.AnnotationCreated]; !ok { + t.Errorf("%s annotation not created", ocispec.AnnotationCreated) + } + + if createdTimeAnnotation, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) + + // Verify creation annotation after time test began + if !nowTime.Before(createdTimeAnnotation) { + t.Errorf("%s annotation with value '%s' not configured properly. Annotation value is not after %s", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated], nowTimeString) + } + + } + +} diff --git a/pkg/helm/pkg/registry/client.go b/pkg/helm/pkg/registry/client.go index 65393601..3295c862 100644 --- a/pkg/helm/pkg/registry/client.go +++ b/pkg/helm/pkg/registry/client.go @@ -14,34 +14,37 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/v3/pkg/registry" +package registry // import "github.com/werf/nelm/pkg/helm/pkg/registry" import ( "context" "crypto/tls" + "crypto/x509" "encoding/json" + "errors" "fmt" "io" + "log/slog" "net/http" + "net/url" + "os" "sort" "strings" "github.com/Masterminds/semver/v3" - "github.com/containerd/containerd/remotes" + "github.com/opencontainers/image-spec/specs-go" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" - "oras.land/oras-go/pkg/auth" - dockerauth "oras.land/oras-go/pkg/auth/docker" - "oras.land/oras-go/pkg/content" - "oras.land/oras-go/pkg/oras" - "oras.land/oras-go/pkg/registry" - registryremote "oras.land/oras-go/pkg/registry/remote" - registryauth "oras.land/oras-go/pkg/registry/remote/auth" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content/memory" + "oras.land/oras-go/v2/registry" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/credentials" + "oras.land/oras-go/v2/registry/remote/retry" "github.com/werf/nelm/pkg/helm/intern/version" - "github.com/werf/nelm/pkg/helm/pkg/chart" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // See https://github.com/helm/helm/issues/10166 @@ -52,22 +55,32 @@ an underscore (_) in chart version tags when pushing to a registry and back to a plus (+) when pulling from a registry.` type ( + // RemoteClient shadows the ORAS remote.Client interface + // (hiding the ORAS type from Helm client visibility) + // https://pkg.go.dev/oras.land/oras-go/pkg/registry/remote#Client + RemoteClient interface { + Do(req *http.Request) (*http.Response, error) + } + // Client works with OCI-compliant registries Client struct { debug bool enableCache bool // path to repository config file e.g. ~/.docker/config.json credentialsFile string + username string + password string out io.Writer - authorizer auth.Client - registryAuthorizer *registryauth.Client - resolver func(ref registry.Reference) (remotes.Resolver, error) + authorizer *auth.Client + registryAuthorizer RemoteClient + credentialsStore credentials.Store httpClient *http.Client plainHTTP bool } // ClientOption allows specifying various settings configurable by the user for overriding the defaults // used when creating a new default client + // TODO(TerryHowe): ClientOption should return error in v5 ClientOption func(*Client) ) @@ -82,89 +95,57 @@ func NewClient(options ...ClientOption) (*Client, error) { if client.credentialsFile == "" { client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename) } - if client.authorizer == nil { - authClient, err := dockerauth.NewClientWithDockerFallback(client.credentialsFile) - if err != nil { - return nil, err + if client.httpClient == nil { + client.httpClient = &http.Client{ + Transport: NewTransport(client.debug), } - client.authorizer = authClient } - resolverFn := client.resolver // copy for avoiding recursive call - client.resolver = func(ref registry.Reference) (remotes.Resolver, error) { - if resolverFn != nil { - // validate if the resolverFn returns a valid resolver - if resolver, err := resolverFn(ref); resolver != nil && err == nil { - return resolver, nil - } - } - headers := http.Header{} - headers.Set("User-Agent", version.GetUserAgent()) - opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} - if client.httpClient != nil { - opts = append(opts, auth.WithResolverClient(client.httpClient)) - } - if client.plainHTTP { - opts = append(opts, auth.WithResolverPlainHTTP()) - - opts = append(opts, func(settings *auth.ResolverSettings) { - settings.Client.Transport = &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - } - }) - } - - resolver, err := client.authorizer.ResolverWithOpts(opts...) - if err != nil { - return nil, err - } - return resolver, nil + storeOptions := credentials.StoreOptions{ + AllowPlaintextPut: true, + DetectDefaultNativeStore: true, } - - // allocate a cache if option is set - var cache registryauth.Cache - if client.enableCache { - cache = registryauth.DefaultCache + store, err := credentials.NewStore(client.credentialsFile, storeOptions) + if err != nil { + return nil, err + } + dockerStore, err := credentials.NewStoreFromDocker(storeOptions) + if err != nil { + // should only fail if user home directory can't be determined + client.credentialsStore = store + } else { + // use Helm credentials with fallback to Docker + client.credentialsStore = credentials.NewStoreWithFallbacks(store, dockerStore) } - if client.registryAuthorizer == nil { - client.registryAuthorizer = ®istryauth.Client{ + + if client.authorizer == nil { + authorizer := auth.Client{ Client: client.httpClient, - Header: http.Header{ - "User-Agent": {version.GetUserAgent()}, - }, - Cache: cache, - Credential: func(ctx context.Context, reg string) (registryauth.Credential, error) { - dockerClient, ok := client.authorizer.(*dockerauth.Client) - if !ok { - return registryauth.EmptyCredential, errors.New("unable to obtain docker client") - } - - username, password, err := dockerClient.Credential(reg) - if err != nil { - return registryauth.EmptyCredential, errors.New("unable to retrieve credentials") - } - - // A blank returned username and password value is a bearer token - if username == "" && password != "" { - return registryauth.Credential{ - RefreshToken: password, - }, nil - } - - return registryauth.Credential{ - Username: username, - Password: password, - }, nil - - }, } + authorizer.SetUserAgent(version.GetUserAgent()) + if client.username != "" && client.password != "" { + authorizer.Credential = func(_ context.Context, _ string) (auth.Credential, error) { + return auth.Credential{Username: client.username, Password: client.password}, nil + } + } else { + authorizer.Credential = credentials.Credential(client.credentialsStore) + } + + if client.enableCache { + authorizer.Cache = auth.NewCache() + } + client.authorizer = &authorizer } + return client, nil } +// Generic returns a GenericClient for low-level OCI operations +func (c *Client) Generic() *GenericClient { + return NewGenericClient(c) +} + // ClientOptDebug returns a function that sets the debug setting on client options set func ClientOptDebug(debug bool) ClientOption { return func(client *Client) { @@ -179,6 +160,14 @@ func ClientOptEnableCache(enableCache bool) ClientOption { } } +// ClientOptBasicAuth returns a function that sets the username and password setting on client options set +func ClientOptBasicAuth(username, password string) ClientOption { + return func(client *Client) { + client.username = username + client.password = password + } +} + // ClientOptWriter returns a function that sets the writer setting on client options set func ClientOptWriter(out io.Writer) ClientOption { return func(client *Client) { @@ -186,6 +175,26 @@ func ClientOptWriter(out io.Writer) ClientOption { } } +// ClientOptAuthorizer returns a function that sets the authorizer setting on a client options set. This +// can be used to override the default authorization mechanism. +// +// Depending on the use-case you may need to set both ClientOptAuthorizer and ClientOptRegistryAuthorizer. +func ClientOptAuthorizer(authorizer auth.Client) ClientOption { + return func(client *Client) { + client.authorizer = &authorizer + } +} + +// ClientOptRegistryAuthorizer returns a function that sets the registry authorizer setting on a client options set. This +// can be used to override the default authorization mechanism. +// +// Depending on the use-case you may need to set both ClientOptAuthorizer and ClientOptRegistryAuthorizer. +func ClientOptRegistryAuthorizer(registryAuthorizer RemoteClient) ClientOption { + return func(client *Client) { + client.registryAuthorizer = registryAuthorizer + } +} + // ClientOptCredentialsFile returns a function that sets the credentialsFile setting on a client options set func ClientOptCredentialsFile(credentialsFile string) ClientOption { return func(client *Client) { @@ -206,74 +215,168 @@ func ClientOptPlainHTTP() ClientOption { } } -// ClientOptResolver returns a function that sets the resolver setting on a client options set -func ClientOptResolver(resolver remotes.Resolver) ClientOption { - return func(client *Client) { - client.resolver = func(ref registry.Reference) (remotes.Resolver, error) { - return resolver, nil - } - } -} - type ( // LoginOption allows specifying various settings on login LoginOption func(*loginOperation) loginOperation struct { - username string - password string - insecure bool - certFile string - keyFile string - caFile string + host string + client *Client } ) +// warnIfHostHasPath checks if the host contains a repository path and logs a warning if it does. +// Returns true if the host contains a path component (i.e., contains a '/'). +func warnIfHostHasPath(host string) bool { + if strings.Contains(host, "/") { + registryHost := strings.Split(host, "/")[0] + slog.Warn("registry login currently only supports registry hostname, not a repository path", "host", host, "suggested", registryHost) + return true + } + return false +} + // Login logs into a registry func (c *Client) Login(host string, options ...LoginOption) error { - operation := &loginOperation{} for _, option := range options { - option(operation) + option(&loginOperation{host, c}) } - authorizerLoginOpts := []auth.LoginOption{ - auth.WithLoginContext(ctx(c.out, c.debug)), - auth.WithLoginHostname(host), - auth.WithLoginUsername(operation.username), - auth.WithLoginSecret(operation.password), - auth.WithLoginUserAgent(version.GetUserAgent()), - auth.WithLoginTLS(operation.certFile, operation.keyFile, operation.caFile), + + warnIfHostHasPath(host) + + reg, err := remote.NewRegistry(host) + if err != nil { + return err } - if operation.insecure { - authorizerLoginOpts = append(authorizerLoginOpts, auth.WithLoginInsecure()) + reg.PlainHTTP = c.plainHTTP + cred := auth.Credential{Username: c.username, Password: c.password} + c.authorizer.ForceAttemptOAuth2 = true + reg.Client = c.authorizer + + ctx := context.Background() + if err := reg.Ping(ctx); err != nil { + c.authorizer.ForceAttemptOAuth2 = false + if err := reg.Ping(ctx); err != nil { + return fmt.Errorf("authenticating to %q: %w", host, err) + } } - if err := c.authorizer.LoginWithOpts(authorizerLoginOpts...); err != nil { + // Always restore to false after probing, to avoid forcing POST to token endpoints like GHCR. + c.authorizer.ForceAttemptOAuth2 = false + + key := credentials.ServerAddressFromRegistry(host) + key = credentials.ServerAddressFromHostname(key) + if err := c.credentialsStore.Put(ctx, key, cred); err != nil { return err } - fmt.Fprintln(c.out, "Login Succeeded") + + _, _ = fmt.Fprintln(c.out, "Login Succeeded") return nil } // LoginOptBasicAuth returns a function that sets the username/password settings on login func LoginOptBasicAuth(username string, password string) LoginOption { - return func(operation *loginOperation) { - operation.username = username - operation.password = password + return func(o *loginOperation) { + o.client.username = username + o.client.password = password + o.client.authorizer.Credential = auth.StaticCredential(o.host, auth.Credential{Username: username, Password: password}) + } +} + +// LoginOptPlainText returns a function that allows plaintext (HTTP) login +func LoginOptPlainText(isPlainText bool) LoginOption { + return func(o *loginOperation) { + o.client.plainHTTP = isPlainText } } +func ensureTLSConfig(client *auth.Client, setConfig *tls.Config) (*tls.Config, error) { + var transport *http.Transport + + switch t := client.Client.Transport.(type) { + case *http.Transport: + transport = t + case *retry.Transport: + switch t := t.Base.(type) { + case *http.Transport: + transport = t + case *LoggingTransport: + switch t := t.RoundTripper.(type) { + case *http.Transport: + transport = t + } + } + } + + if transport == nil { + // we don't know how to access the http.Transport, most likely the + // auth.Client.Client was provided by API user + return nil, fmt.Errorf("unable to access TLS client configuration, the provided HTTP Transport is not supported, given: %T", client.Client.Transport) + } + + switch { + case setConfig != nil: + transport.TLSClientConfig = setConfig + case transport.TLSClientConfig == nil: + transport.TLSClientConfig = &tls.Config{} + } + + return transport.TLSClientConfig, nil +} + // LoginOptInsecure returns a function that sets the insecure setting on login func LoginOptInsecure(insecure bool) LoginOption { - return func(operation *loginOperation) { - operation.insecure = insecure + return func(o *loginOperation) { + tlsConfig, err := ensureTLSConfig(o.client.authorizer, nil) + + if err != nil { + panic(err) + } + + tlsConfig.InsecureSkipVerify = insecure } } // LoginOptTLSClientConfig returns a function that sets the TLS settings on login. func LoginOptTLSClientConfig(certFile, keyFile, caFile string) LoginOption { - return func(operation *loginOperation) { - operation.certFile = certFile - operation.keyFile = keyFile - operation.caFile = caFile + return func(o *loginOperation) { + if (certFile == "" || keyFile == "") && caFile == "" { + return + } + tlsConfig, err := ensureTLSConfig(o.client.authorizer, nil) + if err != nil { + panic(err) + } + + if certFile != "" && keyFile != "" { + authCert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + panic(err) + } + tlsConfig.Certificates = []tls.Certificate{authCert} + } + + if caFile != "" { + certPool := x509.NewCertPool() + ca, err := os.ReadFile(caFile) + if err != nil { + panic(err) + } + if !certPool.AppendCertsFromPEM(ca) { + panic(fmt.Errorf("unable to parse CA file: %q", caFile)) + } + tlsConfig.RootCAs = certPool + } + } +} + +// LoginOptTLSClientConfigFromConfig returns a function that sets the TLS settings on login +// receiving the configuration in memory rather than from files. +func LoginOptTLSClientConfigFromConfig(conf *tls.Config) LoginOption { + return func(o *loginOperation) { + _, err := ensureTLSConfig(o.client.authorizer, conf) + if err != nil { + panic(err) + } } } @@ -290,10 +393,11 @@ func (c *Client) Logout(host string, opts ...LogoutOption) error { for _, opt := range opts { opt(operation) } - if err := c.authorizer.Logout(ctx(c.out, c.debug), host); err != nil { + + if err := credentials.Logout(context.Background(), c.credentialsStore, host); err != nil { return err } - fmt.Fprintf(c.out, "Removing login credentials for %s\n", host) + _, _ = fmt.Fprintf(c.out, "Removing login credentials for %s\n", host) return nil } @@ -328,68 +432,31 @@ type ( } ) -// Pull downloads a chart from a registry -func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { - parsedRef, err := parseReference(ref) - if err != nil { - return nil, err - } +// processChartPull handles chart-specific processing of a generic pull result +func (c *Client) processChartPull(genericResult *GenericPullResult, operation *pullOperation) (*PullResult, error) { + var err error - operation := &pullOperation{ - withChart: true, // By default, always download the chart layer - } - for _, option := range options { - option(operation) - } - if !operation.withChart && !operation.withProv { - return nil, errors.New( - "must specify at least one layer to pull (chart/prov)") - } - memoryStore := content.NewMemory() - allowedMediaTypes := []string{ - ConfigMediaType, - } + // Chart-specific validation minNumDescriptors := 1 // 1 for the config if operation.withChart { minNumDescriptors++ - allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType, LegacyChartLayerMediaType) - } - if operation.withProv { - if !operation.ignoreMissingProv { - minNumDescriptors++ - } - allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType) - } - - var descriptors, layers []ocispec.Descriptor - remotesResolver, err := c.resolver(parsedRef) - if err != nil { - return nil, err } - registryStore := content.Registry{Resolver: remotesResolver} - - manifest, err := oras.Copy(ctx(c.out, c.debug), registryStore, parsedRef.String(), memoryStore, "", - oras.WithPullEmptyNameAllowed(), - oras.WithAllowedMediaTypes(allowedMediaTypes), - oras.WithLayerDescriptors(func(l []ocispec.Descriptor) { - layers = l - })) - if err != nil { - return nil, err + if operation.withProv && !operation.ignoreMissingProv { + minNumDescriptors++ } - descriptors = append(descriptors, manifest) - descriptors = append(descriptors, layers...) - - numDescriptors := len(descriptors) + numDescriptors := len(genericResult.Descriptors) if numDescriptors < minNumDescriptors { return nil, fmt.Errorf("manifest does not contain minimum number of descriptors (%d), descriptors found: %d", minNumDescriptors, numDescriptors) } + + // Find chart-specific descriptors var configDescriptor *ocispec.Descriptor var chartDescriptor *ocispec.Descriptor var provDescriptor *ocispec.Descriptor - for _, descriptor := range descriptors { + + for _, descriptor := range genericResult.Descriptors { d := descriptor switch d.MediaType { case ConfigMediaType: @@ -400,9 +467,11 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { provDescriptor = &d case LegacyChartLayerMediaType: chartDescriptor = &d - fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType) + _, _ = fmt.Fprintf(c.out, "Warning: chart media type %s is deprecated\n", LegacyChartLayerMediaType) } } + + // Chart-specific validation if configDescriptor == nil { return nil, fmt.Errorf("could not load config with mediatype %s", ConfigMediaType) } @@ -410,6 +479,7 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { return nil, fmt.Errorf("manifest does not contain a layer with mediatype %s", ChartLayerMediaType) } + var provMissing bool if operation.withProv && provDescriptor == nil { if operation.ignoreMissingProv { @@ -419,10 +489,12 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { ProvLayerMediaType) } } + + // Build chart-specific result result := &PullResult{ Manifest: &DescriptorPullSummary{ - Digest: manifest.Digest.String(), - Size: manifest.Size, + Digest: genericResult.Manifest.Digest.String(), + Size: genericResult.Manifest.Size, }, Config: &DescriptorPullSummary{ Digest: configDescriptor.Digest.String(), @@ -430,69 +502,94 @@ func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { }, Chart: &DescriptorPullSummaryWithMeta{}, Prov: &DescriptorPullSummary{}, - Ref: parsedRef.String(), + Ref: genericResult.Ref, } - var getManifestErr error - if _, manifestData, ok := memoryStore.Get(manifest); !ok { - getManifestErr = errors.Errorf("Unable to retrieve blob with digest %s", manifest.Digest) - } else { - result.Manifest.Data = manifestData - } - if getManifestErr != nil { - return nil, getManifestErr + + // Fetch data using generic client + genericClient := c.Generic() + + result.Manifest.Data, err = genericClient.GetDescriptorData(genericResult.MemoryStore, genericResult.Manifest) + if err != nil { + return nil, fmt.Errorf("unable to retrieve blob with digest %s: %w", genericResult.Manifest.Digest, err) } - var getConfigDescriptorErr error - if _, configData, ok := memoryStore.Get(*configDescriptor); !ok { - getConfigDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", configDescriptor.Digest) - } else { - result.Config.Data = configData - var meta *chart.Metadata - if err := json.Unmarshal(configData, &meta); err != nil { - return nil, err - } - result.Chart.Meta = meta + + result.Config.Data, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *configDescriptor) + if err != nil { + return nil, fmt.Errorf("unable to retrieve blob with digest %s: %w", configDescriptor.Digest, err) } - if getConfigDescriptorErr != nil { - return nil, getConfigDescriptorErr + + if err := json.Unmarshal(result.Config.Data, &result.Chart.Meta); err != nil { + return nil, err } + if operation.withChart { - var getChartDescriptorErr error - if _, chartData, ok := memoryStore.Get(*chartDescriptor); !ok { - getChartDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", chartDescriptor.Digest) - } else { - result.Chart.Data = chartData - result.Chart.Digest = chartDescriptor.Digest.String() - result.Chart.Size = chartDescriptor.Size - } - if getChartDescriptorErr != nil { - return nil, getChartDescriptorErr + result.Chart.Data, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *chartDescriptor) + if err != nil { + return nil, fmt.Errorf("unable to retrieve blob with digest %s: %w", chartDescriptor.Digest, err) } + result.Chart.Digest = chartDescriptor.Digest.String() + result.Chart.Size = chartDescriptor.Size } + if operation.withProv && !provMissing { - var getProvDescriptorErr error - if _, provData, ok := memoryStore.Get(*provDescriptor); !ok { - getProvDescriptorErr = errors.Errorf("Unable to retrieve blob with digest %s", provDescriptor.Digest) - } else { - result.Prov.Data = provData - result.Prov.Digest = provDescriptor.Digest.String() - result.Prov.Size = provDescriptor.Size - } - if getProvDescriptorErr != nil { - return nil, getProvDescriptorErr + result.Prov.Data, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *provDescriptor) + if err != nil { + return nil, fmt.Errorf("unable to retrieve blob with digest %s: %w", provDescriptor.Digest, err) } + result.Prov.Digest = provDescriptor.Digest.String() + result.Prov.Size = provDescriptor.Size } - fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref) - fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) + _, _ = fmt.Fprintf(c.out, "Pulled: %s\n", result.Ref) + _, _ = fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) if strings.Contains(result.Ref, "_") { - fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) - fmt.Fprint(c.out, registryUnderscoreMessage+"\n") + _, _ = fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) + _, _ = fmt.Fprint(c.out, registryUnderscoreMessage+"\n") } return result, nil } +// Pull downloads a chart from a registry +func (c *Client) Pull(ref string, options ...PullOption) (*PullResult, error) { + operation := &pullOperation{ + withChart: true, // By default, always download the chart layer + } + for _, option := range options { + option(operation) + } + if !operation.withChart && !operation.withProv { + return nil, errors.New( + "must specify at least one layer to pull (chart/prov)") + } + + // Build allowed media types for chart pull + allowedMediaTypes := []string{ + ocispec.MediaTypeImageIndex, + ocispec.MediaTypeImageManifest, + ConfigMediaType, + } + if operation.withChart { + allowedMediaTypes = append(allowedMediaTypes, ChartLayerMediaType, LegacyChartLayerMediaType) + } + if operation.withProv { + allowedMediaTypes = append(allowedMediaTypes, ProvLayerMediaType) + } + + // Use generic client for the pull operation + genericClient := c.Generic() + genericResult, err := genericClient.PullGeneric(ref, GenericPullOptions{ + AllowedMediaTypes: allowedMediaTypes, + }) + if err != nil { + return nil, err + } + + // Process the result with chart-specific logic + return c.processChartPull(genericResult, operation) +} + // PullOptWithChart returns a function that sets the withChart setting on pull func PullOptWithChart(withChart bool) PullOption { return func(operation *pullOperation) { @@ -538,15 +635,15 @@ type ( } pushOperation struct { - provData []byte - strictMode bool - test bool + provData []byte + strictMode bool + creationTime string } ) // Push uploads a chart to a registry. -func (c *Client) Push(data []byte, ref string, opts helmopts.HelmOptions, options ...PushOption) (*PushResult, error) { - parsedRef, err := parseReference(ref) +func (c *Client) Push(data []byte, ref string, options ...PushOption) (*PushResult, error) { + parsedRef, err := newReference(ref) if err != nil { return nil, err } @@ -557,7 +654,7 @@ func (c *Client) Push(data []byte, ref string, opts helmopts.HelmOptions, option for _, option := range options { option(operation) } - meta, err := extractChartMeta(data, opts) + meta, err := extractChartMeta(data) if err != nil { return nil, err } @@ -567,8 +664,11 @@ func (c *Client) Push(data []byte, ref string, opts helmopts.HelmOptions, option "strict mode enabled, ref basename and tag must match the chart name and version") } } - memoryStore := content.NewMemory() - chartDescriptor, err := memoryStore.Add("", ChartLayerMediaType, data) + + ctx := context.Background() + + memoryStore := memory.New() + chartDescriptor, err := oras.PushBytes(ctx, memoryStore, ChartLayerMediaType, data) if err != nil { return nil, err } @@ -578,43 +678,47 @@ func (c *Client) Push(data []byte, ref string, opts helmopts.HelmOptions, option return nil, err } - configDescriptor, err := memoryStore.Add("", ConfigMediaType, configData) + configDescriptor, err := oras.PushBytes(ctx, memoryStore, ConfigMediaType, configData) if err != nil { return nil, err } - descriptors := []ocispec.Descriptor{chartDescriptor} + layers := []ocispec.Descriptor{chartDescriptor} var provDescriptor ocispec.Descriptor if operation.provData != nil { - provDescriptor, err = memoryStore.Add("", ProvLayerMediaType, operation.provData) + provDescriptor, err = oras.PushBytes(ctx, memoryStore, ProvLayerMediaType, operation.provData) if err != nil { return nil, err } - descriptors = append(descriptors, provDescriptor) + layers = append(layers, provDescriptor) } - ociAnnotations := generateOCIAnnotations(meta, operation.test) + // sort layers for determinism, similar to how ORAS v1 does it + sort.Slice(layers, func(i, j int) bool { + return layers[i].Digest < layers[j].Digest + }) - manifestData, manifest, err := content.GenerateManifest(&configDescriptor, ociAnnotations, descriptors...) - if err != nil { - return nil, err - } + ociAnnotations := generateOCIAnnotations(meta, operation.creationTime) - if err := memoryStore.StoreManifest(parsedRef.String(), manifest, manifestData); err != nil { + manifestDescriptor, err := c.tagManifest(ctx, memoryStore, configDescriptor, + layers, ociAnnotations, parsedRef) + if err != nil { return nil, err } - remotesResolver, err := c.resolver(parsedRef) + repository, err := remote.NewRepository(parsedRef.String()) if err != nil { return nil, err } - registryStore := content.Registry{Resolver: remotesResolver} - _, err = oras.Copy(ctx(c.out, c.debug), memoryStore, parsedRef.String(), registryStore, "", - oras.WithNameValidation(nil)) + repository.PlainHTTP = c.plainHTTP + repository.Client = c.authorizer + + manifestDescriptor, err = oras.ExtendedCopy(ctx, memoryStore, parsedRef.String(), repository, parsedRef.String(), oras.DefaultExtendedCopyOptions) if err != nil { return nil, err } + chartSummary := &descriptorPushSummaryWithMeta{ Meta: meta, } @@ -622,8 +726,8 @@ func (c *Client) Push(data []byte, ref string, opts helmopts.HelmOptions, option chartSummary.Size = chartDescriptor.Size result := &PushResult{ Manifest: &descriptorPushSummary{ - Digest: manifest.Digest.String(), - Size: manifest.Size, + Digest: manifestDescriptor.Digest.String(), + Size: manifestDescriptor.Size, }, Config: &descriptorPushSummary{ Digest: configDescriptor.Digest.String(), @@ -639,11 +743,11 @@ func (c *Client) Push(data []byte, ref string, opts helmopts.HelmOptions, option Size: provDescriptor.Size, } } - fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref) - fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) - if strings.Contains(parsedRef.Reference, "_") { - fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) - fmt.Fprint(c.out, registryUnderscoreMessage+"\n") + _, _ = fmt.Fprintf(c.out, "Pushed: %s\n", result.Ref) + _, _ = fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) + if strings.Contains(parsedRef.orasReference.Reference, "_") { + _, _ = fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) + _, _ = fmt.Fprint(c.out, registryUnderscoreMessage+"\n") } return result, err @@ -663,10 +767,10 @@ func PushOptStrictMode(strictMode bool) PushOption { } } -// PushOptTest returns a function that sets whether test setting on push -func PushOptTest(test bool) PushOption { +// PushOptCreationTime returns a function that sets the creation time +func PushOptCreationTime(creationTime string) PushOption { return func(operation *pushOperation) { - operation.test = test + operation.creationTime = creationTime } } @@ -677,27 +781,29 @@ func (c *Client) Tags(ref string) ([]string, error) { return nil, err } - repository := registryremote.Repository{ - Reference: parsedReference, - Client: c.registryAuthorizer, - PlainHTTP: c.plainHTTP, - } - - var registryTags []string - - registryTags, err = registry.Tags(ctx(c.out, c.debug), &repository) + ctx := context.Background() + repository, err := remote.NewRepository(parsedReference.String()) if err != nil { return nil, err } + repository.PlainHTTP = c.plainHTTP + repository.Client = c.authorizer var tagVersions []*semver.Version - for _, tag := range registryTags { - // Change underscore (_) back to plus (+) for Helm - // See https://github.com/helm/helm/issues/10166 - tagVersion, err := semver.StrictNewVersion(strings.ReplaceAll(tag, "_", "+")) - if err == nil { - tagVersions = append(tagVersions, tagVersion) + err = repository.Tags(ctx, "", func(tags []string) error { + for _, tag := range tags { + // Change underscore (_) back to plus (+) for Helm + // See https://github.com/helm/helm/issues/10166 + tagVersion, err := semver.StrictNewVersion(strings.ReplaceAll(tag, "_", "+")) + if err == nil { + tagVersions = append(tagVersions, tagVersion) + } } + + return nil + }) + if err != nil { + return nil, err } // Sort the collection @@ -712,3 +818,111 @@ func (c *Client) Tags(ref string) ([]string, error) { return tags, nil } + +// Resolve a reference to a descriptor. +func (c *Client) Resolve(ref string) (desc ocispec.Descriptor, err error) { + remoteRepository, err := remote.NewRepository(ref) + if err != nil { + return desc, err + } + remoteRepository.PlainHTTP = c.plainHTTP + remoteRepository.Client = c.authorizer + + parsedReference, err := newReference(ref) + if err != nil { + return desc, err + } + + ctx := context.Background() + parsedString := parsedReference.String() + return remoteRepository.Resolve(ctx, parsedString) +} + +// ValidateReference for path and version +func (c *Client) ValidateReference(ref, version string, u *url.URL) (string, *url.URL, error) { + var tag string + + registryReference, err := newReference(u.Host + u.Path) + if err != nil { + return "", nil, err + } + + if version == "" { + // Use OCI URI tag as default + version = registryReference.Tag + } else { + if registryReference.Tag != "" && registryReference.Tag != version { + return "", nil, fmt.Errorf("chart reference and version mismatch: %s is not %s", version, registryReference.Tag) + } + } + + if registryReference.Digest != "" { + if version == "" { + // Install by digest only + return "", u, nil + } + u.Path = fmt.Sprintf("%s@%s", registryReference.Repository, registryReference.Digest) + + // Validate the tag if it was specified + path := registryReference.Registry + "/" + registryReference.Repository + ":" + version + desc, err := c.Resolve(path) + if err != nil { + // The resource does not have to be tagged when digest is specified + return "", u, nil + } + if desc.Digest.String() != registryReference.Digest { + return "", nil, fmt.Errorf("chart reference digest mismatch: %s is not %s", desc.Digest.String(), registryReference.Digest) + } + return registryReference.Digest, u, nil + } + + // Evaluate whether an explicit version has been provided. Otherwise, determine version to use + _, errSemVer := semver.NewVersion(version) + if errSemVer == nil { + tag = version + } else { + // Retrieve list of repository tags + tags, err := c.Tags(strings.TrimPrefix(ref, fmt.Sprintf("%s://", OCIScheme))) + if err != nil { + return "", nil, err + } + if len(tags) == 0 { + return "", nil, fmt.Errorf("unable to locate any tags in provided repository: %s", ref) + } + + // Determine if version provided + // If empty, try to get the highest available tag + // If exact version, try to find it + // If semver constraint string, try to find a match + tag, err = GetTagMatchingVersionOrConstraint(tags, version) + if err != nil { + return "", nil, err + } + } + + u.Path = fmt.Sprintf("%s:%s", registryReference.Repository, tag) + // desc, err := c.Resolve(u.Path) + + return "", u, err +} + +// tagManifest prepares and tags a manifest in memory storage +func (c *Client) tagManifest(ctx context.Context, memoryStore *memory.Store, + configDescriptor ocispec.Descriptor, layers []ocispec.Descriptor, + ociAnnotations map[string]string, parsedRef reference) (ocispec.Descriptor, error) { + + manifest := ocispec.Manifest{ + Versioned: specs.Versioned{SchemaVersion: 2}, + Config: configDescriptor, + Layers: layers, + Annotations: ociAnnotations, + } + + manifestData, err := json.Marshal(manifest) + if err != nil { + return ocispec.Descriptor{}, err + } + + return oras.TagBytes(ctx, memoryStore, ocispec.MediaTypeImageManifest, + manifestData, parsedRef.String()) +} diff --git a/pkg/helm/pkg/registry/client_http_test.go b/pkg/helm/pkg/registry/client_http_test.go index 872d19fc..546d837d 100644 --- a/pkg/helm/pkg/registry/client_http_test.go +++ b/pkg/helm/pkg/registry/client_http_test.go @@ -17,41 +17,51 @@ limitations under the License. package registry import ( + "errors" "fmt" "os" "testing" - "github.com/containerd/containerd/errdefs" "github.com/stretchr/testify/suite" + "oras.land/oras-go/v2/content" ) type HTTPRegistryClientTestSuite struct { - TestSuite + TestRegistry } func (suite *HTTPRegistryClientTestSuite) SetupSuite() { // init test client - dockerRegistry := setup(&suite.TestSuite, false, false) - - // Start Docker registry - go dockerRegistry.ListenAndServe() + setup(&suite.TestRegistry, false, false) } func (suite *HTTPRegistryClientTestSuite) TearDownSuite() { - teardown(&suite.TestSuite) - os.RemoveAll(suite.WorkspaceDir) + teardown(&suite.TestRegistry) + _ = os.RemoveAll(suite.WorkspaceDir) +} + +func (suite *HTTPRegistryClientTestSuite) Test_0_Login() { + err := suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth("badverybad", "ohsobad"), + LoginOptPlainText(true)) + suite.NotNil(err, "error logging into registry with bad credentials") + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth(testUsername, testPassword), + LoginOptPlainText(true)) + suite.Nil(err, "no error logging into registry with good credentials") } func (suite *HTTPRegistryClientTestSuite) Test_1_Push() { - testPush(&suite.TestSuite) + testPush(&suite.TestRegistry) } func (suite *HTTPRegistryClientTestSuite) Test_2_Pull() { - testPull(&suite.TestSuite) + testPull(&suite.TestRegistry) } func (suite *HTTPRegistryClientTestSuite) Test_3_Tags() { - testTags(&suite.TestSuite) + testTags(&suite.TestRegistry) } func (suite *HTTPRegistryClientTestSuite) Test_4_ManInTheMiddle() { @@ -60,7 +70,14 @@ func (suite *HTTPRegistryClientTestSuite) Test_4_ManInTheMiddle() { // returns content that does not match the expected digest _, err := suite.RegistryClient.Pull(ref) suite.NotNil(err) - suite.True(errdefs.IsFailedPrecondition(err)) + suite.True(errors.Is(err, content.ErrMismatchedDigest)) +} + +func (suite *HTTPRegistryClientTestSuite) Test_5_ImageIndex() { + ref := fmt.Sprintf("%s/testrepo/image-index:0.1.0", suite.FakeRegistryHost) + + _, err := suite.RegistryClient.Pull(ref) + suite.Nil(err) } func TestHTTPRegistryClientTestSuite(t *testing.T) { diff --git a/pkg/helm/pkg/registry/client_insecure_tls_test.go b/pkg/helm/pkg/registry/client_insecure_tls_test.go index 5ba79b2e..2774f5e6 100644 --- a/pkg/helm/pkg/registry/client_insecure_tls_test.go +++ b/pkg/helm/pkg/registry/client_insecure_tls_test.go @@ -24,20 +24,17 @@ import ( ) type InsecureTLSRegistryClientTestSuite struct { - TestSuite + TestRegistry } func (suite *InsecureTLSRegistryClientTestSuite) SetupSuite() { // init test client - dockerRegistry := setup(&suite.TestSuite, true, true) - - // Start Docker registry - go dockerRegistry.ListenAndServe() + setup(&suite.TestRegistry, true, true) } func (suite *InsecureTLSRegistryClientTestSuite) TearDownSuite() { - teardown(&suite.TestSuite) - os.RemoveAll(suite.WorkspaceDir) + teardown(&suite.TestRegistry) + _ = os.RemoveAll(suite.WorkspaceDir) } func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() { @@ -53,20 +50,23 @@ func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() { } func (suite *InsecureTLSRegistryClientTestSuite) Test_1_Push() { - testPush(&suite.TestSuite) + testPush(&suite.TestRegistry) } func (suite *InsecureTLSRegistryClientTestSuite) Test_2_Pull() { - testPull(&suite.TestSuite) + testPull(&suite.TestRegistry) } func (suite *InsecureTLSRegistryClientTestSuite) Test_3_Tags() { - testTags(&suite.TestSuite) + testTags(&suite.TestRegistry) } func (suite *InsecureTLSRegistryClientTestSuite) Test_4_Logout() { err := suite.RegistryClient.Logout("this-host-aint-real:5000") - suite.NotNil(err, "error logging out of registry that has no entry") + if err != nil { + // credential backend for mac generates an error + suite.NotNil(err, "failed to delete the credential for this-host-aint-real:5000") + } err = suite.RegistryClient.Logout(suite.DockerRegistryHost) suite.Nil(err, "no error logging out of registry") diff --git a/pkg/helm/pkg/registry/client_test.go b/pkg/helm/pkg/registry/client_test.go new file mode 100644 index 00000000..98a8b2ea --- /dev/null +++ b/pkg/helm/pkg/registry/client_test.go @@ -0,0 +1,168 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "io" + "net/http" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" + "oras.land/oras-go/v2/content/memory" +) + +// Inspired by oras test +// https://github.com/oras-project/oras-go/blob/05a2b09cbf2eab1df691411884dc4df741ec56ab/content_test.go#L1802 +func TestTagManifestTransformsReferences(t *testing.T) { + memStore := memory.New() + client := &Client{out: io.Discard} + ctx := t.Context() + + refWithPlus := "test-registry.io/charts/test:1.0.0+metadata" + expectedRef := "test-registry.io/charts/test:1.0.0_metadata" // + becomes _ + + configDesc := ocispec.Descriptor{MediaType: ConfigMediaType, Digest: "sha256:config", Size: 100} + layers := []ocispec.Descriptor{{MediaType: ChartLayerMediaType, Digest: "sha256:layer", Size: 200}} + + parsedRef, err := newReference(refWithPlus) + require.NoError(t, err) + + desc, err := client.tagManifest(ctx, memStore, configDesc, layers, nil, parsedRef) + require.NoError(t, err) + + transformedDesc, err := memStore.Resolve(ctx, expectedRef) + require.NoError(t, err, "Should find the reference with _ instead of +") + require.Equal(t, desc.Digest, transformedDesc.Digest) + + _, err = memStore.Resolve(ctx, refWithPlus) + require.Error(t, err, "Should NOT find the reference with the original +") +} + +// Verifies that Login always restores ForceAttemptOAuth2 to false on success. +func TestLogin_ResetsForceAttemptOAuth2_OnSuccess(t *testing.T) { + t.Parallel() + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/v2/" { + // Accept either HEAD or GET + w.WriteHeader(http.StatusOK) + return + } + http.NotFound(w, r) + })) + defer srv.Close() + + host := strings.TrimPrefix(srv.URL, "http://") + + credFile := filepath.Join(t.TempDir(), "config.json") + c, err := NewClient( + ClientOptWriter(io.Discard), + ClientOptCredentialsFile(credFile), + ) + if err != nil { + t.Fatalf("NewClient error: %v", err) + } + + if c.authorizer == nil || c.authorizer.ForceAttemptOAuth2 { + t.Fatalf("expected ForceAttemptOAuth2 default to be false") + } + + // Call Login with plain HTTP against our test server + if err := c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p")); err != nil { + t.Fatalf("Login error: %v", err) + } + + if c.authorizer.ForceAttemptOAuth2 { + t.Errorf("ForceAttemptOAuth2 should be false after successful Login") + } +} + +// Verifies that Login restores ForceAttemptOAuth2 to false even when ping fails. +func TestLogin_ResetsForceAttemptOAuth2_OnFailure(t *testing.T) { + t.Parallel() + + // Start and immediately close, so connections will fail + srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) + host := strings.TrimPrefix(srv.URL, "http://") + srv.Close() + + credFile := filepath.Join(t.TempDir(), "config.json") + c, err := NewClient( + ClientOptWriter(io.Discard), + ClientOptCredentialsFile(credFile), + ) + if err != nil { + t.Fatalf("NewClient error: %v", err) + } + + // Invoke Login, expect an error but ForceAttemptOAuth2 must end false + _ = c.Login(host, LoginOptPlainText(true), LoginOptBasicAuth("u", "p")) + + if c.authorizer.ForceAttemptOAuth2 { + t.Errorf("ForceAttemptOAuth2 should be false after failed Login") + } +} + +// TestWarnIfHostHasPath verifies that warnIfHostHasPath correctly detects path components. +func TestWarnIfHostHasPath(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + host string + wantWarn bool + }{ + { + name: "domain only", + host: "ghcr.io", + wantWarn: false, + }, + { + name: "domain with port", + host: "localhost:8000", + wantWarn: false, + }, + { + name: "domain with repository path", + host: "ghcr.io/terryhowe", + wantWarn: true, + }, + { + name: "domain with nested path", + host: "ghcr.io/terryhowe/myrepo", + wantWarn: true, + }, + { + name: "localhost with port and path", + host: "localhost:8000/myrepo", + wantWarn: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := warnIfHostHasPath(tt.host) + if got != tt.wantWarn { + t.Errorf("warnIfHostHasPath(%q) = %v, want %v", tt.host, got, tt.wantWarn) + } + }) + } +} diff --git a/pkg/helm/pkg/registry/client_tls_test.go b/pkg/helm/pkg/registry/client_tls_test.go index 518cfced..ddeeb3b6 100644 --- a/pkg/helm/pkg/registry/client_tls_test.go +++ b/pkg/helm/pkg/registry/client_tls_test.go @@ -17,6 +17,8 @@ limitations under the License. package registry import ( + "crypto/tls" + "crypto/x509" "os" "testing" @@ -24,20 +26,17 @@ import ( ) type TLSRegistryClientTestSuite struct { - TestSuite + TestRegistry } func (suite *TLSRegistryClientTestSuite) SetupSuite() { // init test client - dockerRegistry := setup(&suite.TestSuite, true, false) - - // Start Docker registry - go dockerRegistry.ListenAndServe() + setup(&suite.TestRegistry, true, false) } func (suite *TLSRegistryClientTestSuite) TearDownSuite() { - teardown(&suite.TestSuite) - os.RemoveAll(suite.WorkspaceDir) + teardown(&suite.TestRegistry) + _ = os.RemoveAll(suite.WorkspaceDir) } func (suite *TLSRegistryClientTestSuite) Test_0_Login() { @@ -52,21 +51,48 @@ func (suite *TLSRegistryClientTestSuite) Test_0_Login() { suite.Nil(err, "no error logging into registry with good credentials") } +func (suite *TLSRegistryClientTestSuite) Test_1_Login() { + err := suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth("badverybad", "ohsobad"), + LoginOptTLSClientConfigFromConfig(&tls.Config{})) + suite.NotNil(err, "error logging into registry with bad credentials") + + // Create a *tls.Config from tlsCert, tlsKey, and tlsCA. + cert, err := tls.LoadX509KeyPair(tlsCert, tlsKey) + suite.Nil(err, "error loading x509 key pair") + rootCAs := x509.NewCertPool() + caCert, err := os.ReadFile(tlsCA) + suite.Nil(err, "error reading CA certificate") + rootCAs.AppendCertsFromPEM(caCert) + conf := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: rootCAs, + } + + err = suite.RegistryClient.Login(suite.DockerRegistryHost, + LoginOptBasicAuth(testUsername, testPassword), + LoginOptTLSClientConfigFromConfig(conf)) + suite.Nil(err, "no error logging into registry with good credentials") +} + func (suite *TLSRegistryClientTestSuite) Test_1_Push() { - testPush(&suite.TestSuite) + testPush(&suite.TestRegistry) } func (suite *TLSRegistryClientTestSuite) Test_2_Pull() { - testPull(&suite.TestSuite) + testPull(&suite.TestRegistry) } func (suite *TLSRegistryClientTestSuite) Test_3_Tags() { - testTags(&suite.TestSuite) + testTags(&suite.TestRegistry) } func (suite *TLSRegistryClientTestSuite) Test_4_Logout() { err := suite.RegistryClient.Logout("this-host-aint-real:5000") - suite.NotNil(err, "error logging out of registry that has no entry") + if err != nil { + // credential backend for mac generates an error + suite.NotNil(err, "failed to delete the credential for this-host-aint-real:5000") + } err = suite.RegistryClient.Logout(suite.DockerRegistryHost) suite.Nil(err, "no error logging out of registry") diff --git a/pkg/helm/pkg/registry/constants.go b/pkg/helm/pkg/registry/constants.go index 570b6f0d..940119a5 100644 --- a/pkg/helm/pkg/registry/constants.go +++ b/pkg/helm/pkg/registry/constants.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package registry // import "helm.sh/helm/v3/pkg/registry" +package registry // import "github.com/werf/nelm/pkg/helm/pkg/registry" const ( // OCIScheme is the URL scheme for OCI-based requests diff --git a/pkg/helm/pkg/registry/generic.go b/pkg/helm/pkg/registry/generic.go new file mode 100644 index 00000000..b46133d9 --- /dev/null +++ b/pkg/helm/pkg/registry/generic.go @@ -0,0 +1,161 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "context" + "io" + "net/http" + "slices" + "sort" + "sync" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "oras.land/oras-go/v2" + "oras.land/oras-go/v2/content" + "oras.land/oras-go/v2/content/memory" + "oras.land/oras-go/v2/registry/remote" + "oras.land/oras-go/v2/registry/remote/auth" + "oras.land/oras-go/v2/registry/remote/credentials" +) + +// GenericClient provides low-level OCI operations without artifact-specific assumptions +type GenericClient struct { + debug bool + enableCache bool + credentialsFile string + username string + password string + out io.Writer + authorizer *auth.Client + registryAuthorizer RemoteClient + credentialsStore credentials.Store + httpClient *http.Client + plainHTTP bool +} + +// GenericPullOptions configures a generic pull operation +type GenericPullOptions struct { + // MediaTypes to include in the pull (empty means all) + AllowedMediaTypes []string + // Skip descriptors with these media types + SkipMediaTypes []string + // Custom PreCopy function for filtering + PreCopy func(context.Context, ocispec.Descriptor) error +} + +// GenericPullResult contains the result of a generic pull operation +type GenericPullResult struct { + Manifest ocispec.Descriptor + Descriptors []ocispec.Descriptor + MemoryStore *memory.Store + Ref string +} + +// NewGenericClient creates a new generic OCI client from an existing Client +func NewGenericClient(client *Client) *GenericClient { + return &GenericClient{ + debug: client.debug, + enableCache: client.enableCache, + credentialsFile: client.credentialsFile, + username: client.username, + password: client.password, + out: client.out, + authorizer: client.authorizer, + registryAuthorizer: client.registryAuthorizer, + credentialsStore: client.credentialsStore, + httpClient: client.httpClient, + plainHTTP: client.plainHTTP, + } +} + +// PullGeneric performs a generic OCI pull without artifact-specific assumptions +func (c *GenericClient) PullGeneric(ref string, options GenericPullOptions) (*GenericPullResult, error) { + parsedRef, err := newReference(ref) + if err != nil { + return nil, err + } + + memoryStore := memory.New() + var descriptors []ocispec.Descriptor + + // Set up a repository with authentication and configuration + repository, err := remote.NewRepository(parsedRef.String()) + if err != nil { + return nil, err + } + repository.PlainHTTP = c.plainHTTP + repository.Client = c.authorizer + + ctx := context.Background() + + // Prepare allowed media types for filtering + var allowedMediaTypes []string + if len(options.AllowedMediaTypes) > 0 { + allowedMediaTypes = make([]string, len(options.AllowedMediaTypes)) + copy(allowedMediaTypes, options.AllowedMediaTypes) + sort.Strings(allowedMediaTypes) + } + + var mu sync.Mutex + manifest, err := oras.Copy(ctx, repository, parsedRef.String(), memoryStore, "", oras.CopyOptions{ + CopyGraphOptions: oras.CopyGraphOptions{ + PreCopy: func(ctx context.Context, desc ocispec.Descriptor) error { + // Apply a custom PreCopy function if provided + if options.PreCopy != nil { + if err := options.PreCopy(ctx, desc); err != nil { + return err + } + } + + mediaType := desc.MediaType + + // Skip media types if specified + if slices.Contains(options.SkipMediaTypes, mediaType) { + return oras.SkipNode + } + + // Filter by allowed media types if specified + if len(allowedMediaTypes) > 0 { + if i := sort.SearchStrings(allowedMediaTypes, mediaType); i >= len(allowedMediaTypes) || allowedMediaTypes[i] != mediaType { + return oras.SkipNode + } + } + + mu.Lock() + descriptors = append(descriptors, desc) + mu.Unlock() + return nil + }, + }, + }) + if err != nil { + return nil, err + } + + return &GenericPullResult{ + Manifest: manifest, + Descriptors: descriptors, + MemoryStore: memoryStore, + Ref: parsedRef.String(), + }, nil +} + +// GetDescriptorData retrieves the data for a specific descriptor +func (c *GenericClient) GetDescriptorData(store *memory.Store, desc ocispec.Descriptor) ([]byte, error) { + return content.FetchAll(context.Background(), store, desc) +} diff --git a/pkg/helm/pkg/registry/main_test.go b/pkg/helm/pkg/registry/main_test.go new file mode 100644 index 00000000..4f6e11e4 --- /dev/null +++ b/pkg/helm/pkg/registry/main_test.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "net" + "os" + "testing" + + "github.com/foxcpp/go-mockdns" +) + +func TestMain(m *testing.M) { + // A mock DNS server needed for TLS connection testing. + var srv *mockdns.Server + var err error + + srv, err = mockdns.NewServer(map[string]mockdns.Zone{ + "helm-test-registry.": { + A: []string{"127.0.0.1"}, + }, + }, false) + if err != nil { + panic(err) + } + + saveDialFunction := net.DefaultResolver.Dial + srv.PatchNet(net.DefaultResolver) + + // Run all tests in the package + code := m.Run() + + net.DefaultResolver.Dial = saveDialFunction + _ = srv.Close() + + os.Exit(code) +} diff --git a/pkg/helm/pkg/registry/plugin.go b/pkg/helm/pkg/registry/plugin.go new file mode 100644 index 00000000..e4b4afa2 --- /dev/null +++ b/pkg/helm/pkg/registry/plugin.go @@ -0,0 +1,212 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "encoding/json" + "fmt" + "strings" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +// Plugin-specific constants +const ( + // PluginArtifactType is the artifact type for Helm plugins + PluginArtifactType = "application/vnd.helm.plugin.v1+json" +) + +// PluginPullOptions configures a plugin pull operation +type PluginPullOptions struct { + // PluginName specifies the expected plugin name for layer validation + PluginName string +} + +// PluginPullResult contains the result of a plugin pull operation +type PluginPullResult struct { + Manifest ocispec.Descriptor + PluginData []byte + Prov struct { + Data []byte + } + Ref string + PluginName string +} + +// PullPlugin downloads a plugin from an OCI registry using artifact type +func (c *Client) PullPlugin(ref string, pluginName string, options ...PluginPullOption) (*PluginPullResult, error) { + operation := &pluginPullOperation{ + pluginName: pluginName, + } + for _, option := range options { + option(operation) + } + + // Use generic client for the pull operation with artifact type filtering + genericClient := c.Generic() + genericResult, err := genericClient.PullGeneric(ref, GenericPullOptions{ + // Allow manifests and all layer types - we'll validate artifact type after download + AllowedMediaTypes: []string{ + ocispec.MediaTypeImageManifest, + "application/vnd.oci.image.layer.v1.tar", + "application/vnd.oci.image.layer.v1.tar+gzip", + }, + }) + if err != nil { + return nil, err + } + + // Process the result with plugin-specific logic + return c.processPluginPull(genericResult, operation.pluginName) +} + +// processPluginPull handles plugin-specific processing of a generic pull result using artifact type +func (c *Client) processPluginPull(genericResult *GenericPullResult, pluginName string) (*PluginPullResult, error) { + // First validate that this is actually a plugin artifact + manifestData, err := c.Generic().GetDescriptorData(genericResult.MemoryStore, genericResult.Manifest) + if err != nil { + return nil, fmt.Errorf("unable to retrieve manifest: %w", err) + } + + // Parse the manifest to check artifact type + var manifest ocispec.Manifest + if err := json.Unmarshal(manifestData, &manifest); err != nil { + return nil, fmt.Errorf("unable to parse manifest: %w", err) + } + + // Validate artifact type (for OCI v1.1+ manifests) + if manifest.ArtifactType != "" && manifest.ArtifactType != PluginArtifactType { + return nil, fmt.Errorf("expected artifact type %s, got %s", PluginArtifactType, manifest.ArtifactType) + } + + // For backwards compatibility, also check config media type if no artifact type + if manifest.ArtifactType == "" && manifest.Config.MediaType != PluginArtifactType { + return nil, fmt.Errorf("expected config media type %s for legacy compatibility, got %s", PluginArtifactType, manifest.Config.MediaType) + } + + // Find the plugin tarball and optional provenance using NAME-VERSION.tgz format + var pluginDescriptor *ocispec.Descriptor + var provenanceDescriptor *ocispec.Descriptor + var foundProvenanceName string + + // Look for layers with the expected titles/annotations + for _, layer := range manifest.Layers { + d := layer + // Check for title annotation + if title, exists := d.Annotations[ocispec.AnnotationTitle]; exists { + // Check if this looks like a plugin tarball: {pluginName}-{version}.tgz + if pluginDescriptor == nil && strings.HasPrefix(title, pluginName+"-") && strings.HasSuffix(title, ".tgz") { + pluginDescriptor = &d + } + // Check if this looks like a plugin provenance: {pluginName}-{version}.tgz.prov + if provenanceDescriptor == nil && strings.HasPrefix(title, pluginName+"-") && strings.HasSuffix(title, ".tgz.prov") { + provenanceDescriptor = &d + foundProvenanceName = title + } + } + } + + // Plugin tarball is required + if pluginDescriptor == nil { + return nil, fmt.Errorf("required layer matching pattern %s-VERSION.tgz not found in manifest", pluginName) + } + + // Build plugin-specific result + result := &PluginPullResult{ + Manifest: genericResult.Manifest, + Ref: genericResult.Ref, + PluginName: pluginName, + } + + // Fetch plugin data using generic client + genericClient := c.Generic() + result.PluginData, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *pluginDescriptor) + if err != nil { + return nil, fmt.Errorf("unable to retrieve plugin data with digest %s: %w", pluginDescriptor.Digest, err) + } + + // Fetch provenance data if available + if provenanceDescriptor != nil { + result.Prov.Data, err = genericClient.GetDescriptorData(genericResult.MemoryStore, *provenanceDescriptor) + if err != nil { + return nil, fmt.Errorf("unable to retrieve provenance data with digest %s: %w", provenanceDescriptor.Digest, err) + } + } + + _, _ = fmt.Fprintf(c.out, "Pulled plugin: %s\n", result.Ref) + _, _ = fmt.Fprintf(c.out, "Digest: %s\n", result.Manifest.Digest) + if result.Prov.Data != nil { + _, _ = fmt.Fprintf(c.out, "Provenance: %s\n", foundProvenanceName) + } + + if strings.Contains(result.Ref, "_") { + _, _ = fmt.Fprintf(c.out, "%s contains an underscore.\n", result.Ref) + _, _ = fmt.Fprint(c.out, registryUnderscoreMessage+"\n") + } + + return result, nil +} + +// Plugin pull operation types and options +type ( + pluginPullOperation struct { + pluginName string + withProv bool + } + + // PluginPullOption allows customizing plugin pull operations + PluginPullOption func(*pluginPullOperation) +) + +// PluginPullOptWithPluginName sets the plugin name for validation +func PluginPullOptWithPluginName(name string) PluginPullOption { + return func(operation *pluginPullOperation) { + operation.pluginName = name + } +} + +// GetPluginName extracts the plugin name from an OCI reference using proper reference parsing +func GetPluginName(source string) (string, error) { + ref, err := newReference(source) + if err != nil { + return "", fmt.Errorf("invalid OCI reference: %w", err) + } + + // Extract plugin name from the repository path + // e.g., "ghcr.io/user/plugin-name:v1.0.0" -> Repository: "user/plugin-name" + repository := ref.Repository + if repository == "" { + return "", fmt.Errorf("invalid OCI reference: missing repository") + } + + // Get the last part of the repository path as the plugin name + parts := strings.Split(repository, "/") + pluginName := parts[len(parts)-1] + + if pluginName == "" { + return "", fmt.Errorf("invalid OCI reference: cannot determine plugin name from repository %s", repository) + } + + return pluginName, nil +} + +// PullPluginOptWithProv configures the pull to fetch provenance data +func PullPluginOptWithProv(withProv bool) PluginPullOption { + return func(operation *pluginPullOperation) { + operation.withProv = withProv + } +} diff --git a/pkg/helm/pkg/registry/plugin_test.go b/pkg/helm/pkg/registry/plugin_test.go new file mode 100644 index 00000000..f8525829 --- /dev/null +++ b/pkg/helm/pkg/registry/plugin_test.go @@ -0,0 +1,93 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "testing" +) + +func TestGetPluginName(t *testing.T) { + tests := []struct { + name string + source string + expected string + expectErr bool + }{ + { + name: "valid OCI reference with tag", + source: "oci://ghcr.io/user/plugin-name:v1.0.0", + expected: "plugin-name", + }, + { + name: "valid OCI reference with digest", + source: "oci://ghcr.io/user/plugin-name@sha256:1234567890abcdef", + expected: "plugin-name", + }, + { + name: "valid OCI reference without tag", + source: "oci://ghcr.io/user/plugin-name", + expected: "plugin-name", + }, + { + name: "valid OCI reference with multiple path segments", + source: "oci://registry.example.com/org/team/plugin-name:latest", + expected: "plugin-name", + }, + { + name: "valid OCI reference with plus signs in tag", + source: "oci://registry.example.com/user/plugin-name:v1.0.0+build.1", + expected: "plugin-name", + }, + { + name: "valid OCI reference - single path segment", + source: "oci://registry.example.com/plugin", + expected: "plugin", + }, + { + name: "invalid OCI reference - no repository", + source: "oci://registry.example.com", + expectErr: true, + }, + { + name: "invalid OCI reference - malformed", + source: "not-an-oci-reference", + expectErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pluginName, err := GetPluginName(tt.source) + + if tt.expectErr { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + if pluginName != tt.expected { + t.Errorf("expected plugin name %q, got %q", tt.expected, pluginName) + } + }) + } +} diff --git a/pkg/helm/pkg/registry/reference.go b/pkg/helm/pkg/registry/reference.go new file mode 100644 index 00000000..9a98cf5c --- /dev/null +++ b/pkg/helm/pkg/registry/reference.go @@ -0,0 +1,84 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "fmt" + "strings" + + "oras.land/oras-go/v2/registry" +) + +type reference struct { + orasReference registry.Reference + Registry string + Repository string + Tag string + Digest string +} + +// newReference will parse and validate the reference, and clean tags when +// applicable tags are only cleaned when plus (+) signs are present and are +// converted to underscores (_) before pushing +// See https://github.com/helm/helm/issues/10166 +func newReference(raw string) (result reference, err error) { + // Remove the oci:// prefix if it is there + raw = strings.TrimPrefix(raw, OCIScheme+"://") + + // The sole possible reference modification is replacing plus (+) signs + // present in tags with underscores (_). To do this properly, we first + // need to identify a tag, and then pass it on to the reference parser + // NOTE: Passing immediately to the reference parser will fail since (+) + // signs are an invalid tag character, and simply replacing all plus (+) + // occurrences could invalidate other portions of the URI + lastIndex := strings.LastIndex(raw, "@") + if lastIndex >= 0 { + result.Digest = raw[(lastIndex + 1):] + raw = raw[:lastIndex] + } + parts := strings.Split(raw, ":") + if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { + tag := parts[len(parts)-1] + + if tag != "" { + // Replace any plus (+) signs with known underscore (_) conversion + newTag := strings.ReplaceAll(tag, "+", "_") + raw = strings.ReplaceAll(raw, tag, newTag) + } + } + + result.orasReference, err = registry.ParseReference(raw) + if err != nil { + return result, err + } + result.Registry = result.orasReference.Registry + result.Repository = result.orasReference.Repository + result.Tag = result.orasReference.Reference + return result, nil +} + +func (r *reference) String() string { + if r.Tag == "" { + return r.orasReference.String() + "@" + r.Digest + } + return r.orasReference.String() +} + +// IsOCI determines whether a URL is to be treated as an OCI URL +func IsOCI(url string) bool { + return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) +} diff --git a/pkg/helm/pkg/registry/reference_test.go b/pkg/helm/pkg/registry/reference_test.go new file mode 100644 index 00000000..b6872cc3 --- /dev/null +++ b/pkg/helm/pkg/registry/reference_test.go @@ -0,0 +1,100 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import "testing" + +func verify(t *testing.T, actual reference, registry, repository, tag, digest string) { + t.Helper() + if registry != actual.orasReference.Registry { + t.Errorf("Oras reference registry expected %v actual %v", registry, actual.Registry) + } + if repository != actual.orasReference.Repository { + t.Errorf("Oras reference repository expected %v actual %v", repository, actual.Repository) + } + if tag != actual.orasReference.Reference { + t.Errorf("Oras reference reference expected %v actual %v", tag, actual.Tag) + } + if registry != actual.Registry { + t.Errorf("Registry expected %v actual %v", registry, actual.Registry) + } + if repository != actual.Repository { + t.Errorf("Repository expected %v actual %v", repository, actual.Repository) + } + if tag != actual.Tag { + t.Errorf("Tag expected %v actual %v", tag, actual.Tag) + } + if digest != actual.Digest { + t.Errorf("Digest expected %v actual %v", digest, actual.Digest) + } + expectedString := registry + if repository != "" { + expectedString = expectedString + "/" + repository + } + if tag != "" { + expectedString = expectedString + ":" + tag + } else { + expectedString = expectedString + "@" + digest + } + if actual.String() != expectedString { + t.Errorf("String expected %s actual %s", expectedString, actual.String()) + } +} + +func TestNewReference(t *testing.T) { + actual, err := newReference("registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + + actual, err = newReference("oci://registry.example.com/repository:1.0@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "repository", "1.0", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + + actual, err = newReference("a/b:1@c") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "a", "b", "1", "c") + + actual, err = newReference("a/b:@") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "a", "b", "", "") + + actual, err = newReference("registry.example.com/repository:1.0+001") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "repository", "1.0_001", "") + + actual, err = newReference("thing:1.0") + if err == nil { + t.Errorf("Expect error error %v", err) + } + verify(t, actual, "", "", "", "") + + actual, err = newReference("registry.example.com/the/repository@sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") + if err != nil { + t.Errorf("Unexpected error %v", err) + } + verify(t, actual, "registry.example.com", "the/repository", "", "sha256:c6841b3a895f1444a6738b5d04564a57e860ce42f8519c3be807fb6d9bee7888") +} diff --git a/pkg/helm/pkg/registry/registry_test.go b/pkg/helm/pkg/registry/registry_test.go new file mode 100644 index 00000000..873c1f12 --- /dev/null +++ b/pkg/helm/pkg/registry/registry_test.go @@ -0,0 +1,560 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "strings" + "time" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "golang.org/x/crypto/bcrypt" + + "github.com/werf/nelm/pkg/helm/intern/tlsutil" +) + +const ( + tlsServerKey = "./testdata/tls/server.key" + tlsServerCert = "./testdata/tls/server.crt" + tlsCA = "./testdata/tls/ca.crt" + tlsKey = "./testdata/tls/client.key" + tlsCert = "./testdata/tls/client.crt" +) + +var ( + testWorkspaceDir = "helm-registry-test" + testHtpasswdFileBasename = "authtest.htpasswd" + testUsername = "myuser" + testPassword = "mypass" +) + +type TestRegistry struct { + suite.Suite + Out io.Writer + FakeRegistryHost string + DockerRegistryHost string + CompromisedRegistryHost string + WorkspaceDir string + RegistryClient *Client + dockerRegistry *registry.Registry +} + +func setup(suite *TestRegistry, tlsEnabled, insecure bool) { + suite.WorkspaceDir = testWorkspaceDir + err := os.RemoveAll(suite.WorkspaceDir) + require.NoError(suite.T(), err, "no error removing test workspace dir") + err = os.Mkdir(suite.WorkspaceDir, 0700) + require.NoError(suite.T(), err, "no error creating test workspace dir") + + var out bytes.Buffer + + suite.Out = &out + credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename) + + // init test client + opts := []ClientOption{ + ClientOptDebug(true), + ClientOptEnableCache(true), + ClientOptWriter(suite.Out), + ClientOptCredentialsFile(credentialsFile), + ClientOptBasicAuth(testUsername, testPassword), + } + + if tlsEnabled { + var tlsConf *tls.Config + if insecure { + tlsConf, err = tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(true), + ) + } else { + tlsConf, err = tlsutil.NewTLSConfig( + tlsutil.WithCertKeyPairFiles(tlsCert, tlsKey), + tlsutil.WithCAFile(tlsCA), + ) + } + httpClient := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConf, + }, + } + suite.Nil(err, "no error loading tls config") + opts = append(opts, ClientOptHTTPClient(httpClient)) + } else { + opts = append(opts, ClientOptPlainHTTP()) + } + + suite.RegistryClient, err = NewClient(opts...) + suite.Nil(err, "no error creating registry client") + + // create htpasswd file (w BCrypt, which is required) + pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + suite.Nil(err, "no error generating bcrypt password for test htpasswd file") + htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename) + err = os.WriteFile(htpasswdPath, fmt.Appendf(nil, "%s:%s\n", testUsername, string(pwBytes)), 0644) + suite.Nil(err, "no error creating test htpasswd file") + + // Registry config + config := &configuration.Configuration{} + ln, err := net.Listen("tcp", "127.0.0.1:0") + suite.Nil(err, "no error finding free port for test registry") + defer func() { _ = ln.Close() }() + + // Change the registry host to another host which is not localhost. + // This is required because Docker enforces HTTP if the registry + // host is localhost/127.0.0.1. + port := ln.Addr().(*net.TCPAddr).Port + suite.DockerRegistryHost = fmt.Sprintf("helm-test-registry:%d", port) + + config.HTTP.Addr = ln.Addr().String() + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + + // config tls + if tlsEnabled { + // TLS config + // this set tlsConf.ClientAuth = tls.RequireAndVerifyClientCert in the + // server tls config + config.HTTP.TLS.Certificate = tlsServerCert + config.HTTP.TLS.Key = tlsServerKey + // Skip client authentication if the registry is insecure. + if !insecure { + config.HTTP.TLS.ClientCAs = []string{tlsCA} + } + } + suite.dockerRegistry, err = registry.NewRegistry(context.Background(), config) + suite.Nil(err, "no error creating test registry") + + suite.FakeRegistryHost = initFakeRegistryTestServer() + suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() + go func() { + _ = suite.dockerRegistry.ListenAndServe() + }() +} + +func teardown(suite *TestRegistry) { + if suite.dockerRegistry != nil { + _ = suite.dockerRegistry.Shutdown(context.Background()) + } +} + +func initCompromisedRegistryTestServer() string { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "manifests") { + w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json") + w.WriteHeader(http.StatusOK) + + _, _ = fmt.Fprintf(w, `{ "schemaVersion": 2, "config": { + "mediaType": "%s", + "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133", + "size": 181 + }, + "layers": [ + { + "mediaType": "%s", + "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", + "size": 1 + } + ] +}`, ConfigMediaType, ChartLayerMediaType) + } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" + + "an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" + + "\"application\"}")) + } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" { + w.Header().Set("Content-Type", ChartLayerMediaType) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("b")) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + })) + + u, _ := url.Parse(s.URL) + return fmt.Sprintf("localhost:%s", u.Port()) +} + +func initFakeRegistryTestServer() string { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/testrepo/image-index/manifests/0.1.0": + w.Header().Set("Content-Type", ocispec.MediaTypeImageIndex) + w.Write([]byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:2771e37a12b7bcb2902456ecf3f29bf9ee11ec348e66e8eb322d9780ad7fc2df", + "size": 1035, + "platform": { + "architecture": "amd64", + "os": "linux" + }, + "annotations": { + "com.docker.official-images.bashbrew.arch": "amd64", + "org.opencontainers.image.base.name": "scratch", + "org.opencontainers.image.created": "2025-08-13T22:16:57Z", + "org.opencontainers.image.revision": "6930d60e10e81283a57be3ee3a2b5ca328a40304", + "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#6930d60e10e81283a57be3ee3a2b5ca328a40304:amd64/hello-world", + "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world", + "org.opencontainers.image.version": "linux" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:6b75187531c5e9b6a85c8946d5d82e4ef3801e051fbff338f382f3edfa60e3d2", + "size": 566, + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "annotations": { + "com.docker.official-images.bashbrew.arch": "amd64", + "vnd.docker.reference.digest": "sha256:2771e37a12b7bcb2902456ecf3f29bf9ee11ec348e66e8eb322d9780ad7fc2df", + "vnd.docker.reference.type": "attestation-manifest" + } + }, + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:7fbdc47de56b45d092f8f419e8b6183adf0159d00e05574c01787231b54fe28f", + "size": 815 + } + ] +}`)) + + case "/v2/testrepo/image-index/manifests/sha256:2771e37a12b7bcb2902456ecf3f29bf9ee11ec348e66e8eb322d9780ad7fc2df": + w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest) + w.Write([]byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:1b44b5a3e06a9aae883e7bf25e45c100be0bb81a0e01b32de604f3ac44711634", + "size": 547 + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "digest": "sha256:17eec7bbc9d79fa397ac95c7283ecd04d1fe6978516932a3db110c6206430809", + "size": 2380 + } + ], + "annotations": { + "com.docker.official-images.bashbrew.arch": "amd64", + "org.opencontainers.image.base.name": "scratch", + "org.opencontainers.image.created": "2025-08-08T19:05:17Z", + "org.opencontainers.image.revision": "6930d60e10e81283a57be3ee3a2b5ca328a40304", + "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#6930d60e10e81283a57be3ee3a2b5ca328a40304:amd64/hello-world", + "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world", + "org.opencontainers.image.version": "linux" + } +}`)) + + case "/v2/testrepo/image-index/manifests/sha256:6b75187531c5e9b6a85c8946d5d82e4ef3801e051fbff338f382f3edfa60e3d2": + w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest) + w.Write([]byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "digest": "sha256:ec4b6233950725be4c816667d1eb2782ad59dc65b12f7ac53f1ffa0ad5b95b5b", + "size": 167 + }, + "layers": [ + { + "mediaType": "application/vnd.in-toto+json", + "digest": "sha256:ea52d2000f90ad63267302cba134025ee586b07a63c47aa9467471a395aee6c2", + "size": 4822, + "annotations": { + "in-toto.io/predicate-type": "https://slsa.dev/provenance/v0.2" + } + } + ] +}`)) + + case "/v2/testrepo/image-index/manifests/sha256:7fbdc47de56b45d092f8f419e8b6183adf0159d00e05574c01787231b54fe28f": + w.Header().Set("Content-Type", ocispec.MediaTypeImageManifest) + w.Write([]byte(`{ + "schemaVersion": 2, + "config": { + "mediaType": "application/vnd.cncf.helm.config.v1+json", + "digest": "sha256:24de43e4a9f5ed9427479f27dd7bab9d158227abe593302a6f54d1e13a903ac3", + "size": 112 + }, + "layers": [ + { + "mediaType": "application/vnd.cncf.helm.chart.provenance.v1.prov", + "digest": "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", + "size": 695 + }, + { + "mediaType": "application/vnd.cncf.helm.chart.content.v1.tar+gzip", + "digest": "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + "size": 973 + } + ], + "annotations": { + "org.opencontainers.image.description": "A Helm chart for Kubernetes", + "org.opencontainers.image.title": "signtest", + "org.opencontainers.image.version": "0.1.0" + } +}`)) + + case "/v2/testrepo/image-index/blobs/sha256:24de43e4a9f5ed9427479f27dd7bab9d158227abe593302a6f54d1e13a903ac3": + w.Header().Set("Content-Type", ConfigMediaType) + w.Write([]byte(`{ + "name":"signtest", + "version":"0.1.0", + "description":"A Helm chart for Kubernetes", + "apiVersion":"v1" +}`)) + + case "/v2/testrepo/image-index/blobs/sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256": + data, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Header().Set("Content-Type", ProvLayerMediaType) + w.Write(data) + + case "/v2/testrepo/image-index/blobs/sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55": + data, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(err.Error())) + return + } + + w.Header().Set("Content-Type", ChartLayerMediaType) + w.Write(data) + + default: + w.WriteHeader(http.StatusNotFound) + } + })) + + u, _ := url.Parse(s.URL) + return fmt.Sprintf("localhost:%s", u.Port()) +} + +func testPush(suite *TestRegistry) { + + testingChartCreationTime := "1977-09-02T22:04:05Z" + + // Bad bytes + ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost) + _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptCreationTime(testingChartCreationTime)) + suite.NotNil(err, "error pushing non-chart bytes") + + // Load a test chart + chartData, err := os.ReadFile("../repo/v1/repotest/testdata/examplechart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err := extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + + // non-strict ref (chart name) + ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) + suite.NotNil(err, "error pushing non-strict ref (bad basename)") + + // non-strict ref (chart name), with strict mode disabled + _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime)) + suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") + + // non-strict ref (chart version) + ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) + suite.NotNil(err, "error pushing non-strict ref (bad tag)") + + // non-strict ref (chart version), with strict mode disabled + _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptCreationTime(testingChartCreationTime)) + suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") + + // basic push, good ref + chartData, err = os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err = extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + _, err = suite.RegistryClient.Push(chartData, ref, PushOptCreationTime(testingChartCreationTime)) + suite.Nil(err, "no error pushing good ref") + + _, err = suite.RegistryClient.Pull(ref) + suite.Nil(err, "no error pulling a simple chart") + + // Load another test chart + chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err = extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + + // Load prov file + provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + suite.Nil(err, "no error loading test prov") + + // push with prov + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptCreationTime(testingChartCreationTime)) + suite.Nil(err, "no error pushing good ref with prov") + + _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true)) + suite.Nil(err, "no error pulling a simple chart") + + // Validate the output + // Note: these digests/sizes etc may change if the test chart/prov files are modified, + // or if the format of the OCI manifest changes + suite.Equal(ref, result.Ref) + suite.Equal(meta.Name, result.Chart.Meta.Name) + suite.Equal(meta.Version, result.Chart.Meta.Version) + suite.Equal(int64(742), result.Manifest.Size) + suite.Equal(int64(99), result.Config.Size) + suite.Equal(int64(973), result.Chart.Size) + suite.Equal(int64(695), result.Prov.Size) + suite.Equal( + "sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2", + result.Manifest.Digest) + suite.Equal( + "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", + result.Config.Digest) + suite.Equal( + "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + result.Chart.Digest) + suite.Equal( + "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", + result.Prov.Digest) +} + +func testPull(suite *TestRegistry) { + // bad/missing ref + ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost) + _, err := suite.RegistryClient.Pull(ref) + suite.NotNil(err, "error on bad/missing ref") + + // Load test chart (to build ref pushed in previous test) + chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err := extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + + // Simple pull, chart only + _, err = suite.RegistryClient.Pull(ref) + suite.Nil(err, "no error pulling a simple chart") + + // Simple pull with prov (no prov uploaded) + _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true)) + suite.NotNil(err, "error pulling a chart with prov when no prov exists") + + // Simple pull with prov, ignoring missing prov + _, err = suite.RegistryClient.Pull(ref, + PullOptWithProv(true), + PullOptIgnoreMissingProv(true)) + suite.Nil(err, + "no error pulling a chart with prov when no prov exists, ignoring missing") + + // Load test chart (to build ref pushed in previous test) + chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err = extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) + + // Load prov file + provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") + suite.Nil(err, "no error loading test prov") + + // no chart and no prov causes error + _, err = suite.RegistryClient.Pull(ref, + PullOptWithChart(false), + PullOptWithProv(false)) + suite.NotNil(err, "error on both no chart and no prov") + + // full pull with chart and prov + result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true)) + suite.Require().Nil(err, "no error pulling a chart with prov") + + // Validate the output + // Note: these digests/sizes etc may change if the test chart/prov files are modified, + // or if the format of the OCI manifest changes + suite.Equal(ref, result.Ref) + suite.Equal(meta.Name, result.Chart.Meta.Name) + suite.Equal(meta.Version, result.Chart.Meta.Version) + suite.Equal(int64(742), result.Manifest.Size) + suite.Equal(int64(99), result.Config.Size) + suite.Equal(int64(973), result.Chart.Size) + suite.Equal(int64(695), result.Prov.Size) + suite.Equal( + "sha256:fbbade96da6050f68f94f122881e3b80051a18f13ab5f4081868dd494538f5c2", + result.Manifest.Digest) + suite.Equal( + "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", + result.Config.Digest) + suite.Equal( + "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", + result.Chart.Digest) + suite.Equal( + "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", + result.Prov.Digest) + suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.created\":\"1977-09-02T22:04:05Z\",\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}", + string(result.Manifest.Data)) + suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}", + string(result.Config.Data)) + suite.Equal(chartData, result.Chart.Data) + suite.Equal(provData, result.Prov.Data) +} + +func testTags(suite *TestRegistry) { + // Load test chart (to build ref pushed in previous test) + chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") + suite.Nil(err, "no error loading test chart") + meta, err := extractChartMeta(chartData) + suite.Nil(err, "no error extracting chart meta") + ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name) + + // Query for tags and validate length + tags, err := suite.RegistryClient.Tags(ref) + suite.Nil(err, "no error retrieving tags") + suite.Equal(1, len(tags)) +} diff --git a/pkg/helm/pkg/registry/tag.go b/pkg/helm/pkg/registry/tag.go new file mode 100644 index 00000000..39e94c99 --- /dev/null +++ b/pkg/helm/pkg/registry/tag.go @@ -0,0 +1,59 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry // import "github.com/werf/nelm/pkg/helm/pkg/registry" + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" +) + +func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { + var constraint *semver.Constraints + if versionString == "" { + // If the string is empty, set a wildcard constraint + constraint, _ = semver.NewConstraint("*") + } else { + // when customer inputs a specific version, check whether there's an exact match first + for _, v := range tags { + if versionString == v { + return v, nil + } + } + + // Otherwise set constraint to the string given + var err error + constraint, err = semver.NewConstraint(versionString) + if err != nil { + return "", err + } + } + + // Otherwise try to find the first available version matching the string, + // in case it is a constraint + for _, v := range tags { + test, err := semver.NewVersion(v) + if err != nil { + continue + } + if constraint.Check(test) { + return v, nil + } + } + + return "", fmt.Errorf("could not locate a version matching provided version string %s", versionString) +} diff --git a/pkg/helm/pkg/registry/tag_test.go b/pkg/helm/pkg/registry/tag_test.go new file mode 100644 index 00000000..09f0f12e --- /dev/null +++ b/pkg/helm/pkg/registry/tag_test.go @@ -0,0 +1,122 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "strings" + "testing" +) + +func TestGetTagMatchingVersionOrConstraint_ExactMatch(t *testing.T) { + tags := []string{"1.0.0", "1.2.3", "2.0.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.2.3" { + t.Fatalf("expected exact match '1.2.3', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_EmptyVersionWildcard(t *testing.T) { + // Includes a non-semver tag which should be skipped + tags := []string{"latest", "0.9.0", "1.0.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Should pick the first valid semver tag in order, which is 0.9.0 + if got != "0.9.0" { + t.Fatalf("expected '0.9.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_ConstraintRange(t *testing.T) { + tags := []string{"0.5.0", "1.0.0", "1.1.0", "2.0.0"} + + // Caret range + got, err := GetTagMatchingVersionOrConstraint(tags, "^1.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { // first match in order + t.Fatalf("expected '1.0.0', got %q", got) + } + + // Compound range + got, err = GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { + t.Fatalf("expected '1.0.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_InvalidConstraint(t *testing.T) { + tags := []string{"1.0.0"} + _, err := GetTagMatchingVersionOrConstraint(tags, ">a1") + if err == nil { + t.Fatalf("expected error for invalid constraint") + } +} + +func TestGetTagMatchingVersionOrConstraint_NoMatches(t *testing.T) { + tags := []string{"0.1.0", "0.2.0"} + _, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0") + if err == nil { + t.Fatalf("expected error when no tags match") + } + if !strings.Contains(err.Error(), ">=1.0.0") { + t.Fatalf("expected error to contain version string, got: %v", err) + } +} + +func TestGetTagMatchingVersionOrConstraint_SkipsNonSemverTags(t *testing.T) { + tags := []string{"alpha", "1.0.0", "beta", "1.1.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { + t.Fatalf("expected '1.0.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_OrderMatters_FirstMatchReturned(t *testing.T) { + // Both 1.2.0 and 1.3.0 satisfy >=1.2.0 <2.0.0, but the function returns the first in input order + tags := []string{"1.3.0", "1.2.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.2.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.3.0" { + t.Fatalf("expected '1.3.0' (first satisfying tag), got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_ExactMatchHasPrecedence(t *testing.T) { + // Exact match should be returned even if another earlier tag would match the parsed constraint + tags := []string{"1.3.0", "1.2.3"} + got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.2.3" { + t.Fatalf("expected exact match '1.2.3', got %q", got) + } +} diff --git a/pkg/helm/pkg/registry/testdata/tls/ca.crt b/pkg/helm/pkg/registry/testdata/tls/ca.crt index d5b845ac..8c46ff81 100644 --- a/pkg/helm/pkg/registry/testdata/tls/ca.crt +++ b/pkg/helm/pkg/registry/testdata/tls/ca.crt @@ -1,21 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIDhzCCAm+gAwIBAgIUEtjKXd8LxpkQf3C5LgdzM1++R3swDQYJKoZIhvcNAQEL +MIIDiTCCAnGgAwIBAgIUbTTp/VG6blpKnXwWpSVtw54jxzswDQYJKoZIhvcNAQEL BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG -A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTIzMDYw -ODEwNDkzOFoXDTI0MDYwNzEwNDkzOFowUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgM -AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwM -QWNtZSBSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApgrX -Lv3k3trxje2JEoqusYN67Z3byZg69djRatfdboS3JKoTIHtcY7MMLdfhjAK97/wv -BaIMuVNgueu4qH6bea7FCP8XWz2BYBrH2GcKjVrBMkUrlIzjG9gnohkeknJQvQvl -oVbqLgZJn0HQcZtsPDnLwfjWDZrNkFBtvPSIMaRQbmtOFdSqAQjLKezbwlznBCJ5 -qpLsgc67ttDW5QAS+GszWPmypUlw8Ih7m8J95eT9aUESP0DbdraeUktWJQTdqukd -NflLaA2ZoV+uTX+wVE4yyXgSjD3Sd93+XhoSSzDzkzRnLsocRutxrTiNC/1S+qhb -Z72XLk0bvNwQhJjHDQIDAQABo1MwUTAdBgNVHQ4EFgQUoSKAVvuJDGszE361K7IF -RXOVj2YwHwYDVR0jBBgwFoAUoSKAVvuJDGszE361K7IFRXOVj2YwDwYDVR0TAQH/ -BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOqH/JFuT1sqY/zVxCsATE1ze85/o -r6yPw3AuXsFzWtHe/XOFJzvbfOBWfocVLXTDc5933f1Ws/+PcxQKEQCwnUHrEAso -jLPzy+igHc07pi9PqHJ21Sn8FF5JVv+Y6CcZKaF5aEzUISsVjbF2vGK8FotMS9rs -Jw//dDfKhHjO9MHPBdkhOrM31LV6gwYPepno/YYygrJwHGQ5V9sdY8ifRBG6lX2a -xK4N2bl5q3Cpz+iERLNGP2c8OVQwLfSYLpFRSbHS8UiN4z6WqfgYHG7YurvbiMiJ -/AFkUatVJQ5YLmfCz4FMAiaxNtEOkZh5cvL1eCLK7nzvgAPCI33mEp6eoA== +A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMCAXDTI0MDQy +MTA5NDUxOFoYDzMzOTMwNDA0MDk0NTE4WjBTMQswCQYDVQQGEwJDTjELMAkGA1UE +CAwCR0QxCzAJBgNVBAcMAlNaMRMwEQYDVQQKDApBY21lLCBJbmMuMRUwEwYDVQQD +DAxBY21lIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCq +OrCgXpMjeSjWJYanmSG/2K4zk0HXeU3eMt5bkshlqHnEwJFD5tMZkJZUsGPiJr9A +vAqYu2V9/gMKUptvHgxmMkh9BZYCnXAGzhl+OogYcJA5l/YBuDvmgz8M3aRZr7xd +IA9KtepnDlp7NRWXsgRHzJNMBkV4PpEVHbJTVdjHVYERCw0C1kcb6wjzshnmUmJJ +JVEQDRCCaYymtIymR6kKrZzIw2FeyXxcccbvTsKILItEECYmRNevo1mc5/f8BEXx +IzEPhDpoKSTq5JjWHCQH1shkwWyg2neL7g0UJ8nyV0pqqScE0L1WUZ1BHnVJAmGm +R61WXxA3xCFzJHSc2enRAgMBAAGjUzBRMB0GA1UdDgQWBBREgz+BR+lJFNaG2D7+ +tDVzzyjc4jAfBgNVHSMEGDAWgBREgz+BR+lJFNaG2D7+tDVzzyjc4jAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAL9DjzmNwDljtMWvwAbDC11bIw +zHON10J/bLcoZy3r7SaD1ZjPigzdpd0oVaoq+Kcg/J0JuIN2fBzyFljft//9knDA +GgO4TvDdd7dk4gv6C/fbmeh+/HsnjRDHQmExzgth5akSnmtxyk5HQR72FrWICqjf +oEqg8xs0gVwl8Z0xXLgJ7BZEzRxYlV/G2+vjA1FYIGd3Qfiyg8Qd68Y5bs2/HdBC +a0EteVUNhS1XVjFFxDZnegPKZs30RwDHcVt9Pj/dLVXu2BgtdYupWtMbtfXNmsg2 +pJcFk7Ve1CAtfrQ2t8DAwOpKHkKIqExupQaGwbdTAtNiQtdGntv4oHuEGJ9p -----END CERTIFICATE----- diff --git a/pkg/helm/pkg/registry/testdata/tls/ca.key b/pkg/helm/pkg/registry/testdata/tls/ca.key new file mode 100644 index 00000000..f228b4d2 --- /dev/null +++ b/pkg/helm/pkg/registry/testdata/tls/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCqOrCgXpMjeSjW +JYanmSG/2K4zk0HXeU3eMt5bkshlqHnEwJFD5tMZkJZUsGPiJr9AvAqYu2V9/gMK +UptvHgxmMkh9BZYCnXAGzhl+OogYcJA5l/YBuDvmgz8M3aRZr7xdIA9KtepnDlp7 +NRWXsgRHzJNMBkV4PpEVHbJTVdjHVYERCw0C1kcb6wjzshnmUmJJJVEQDRCCaYym +tIymR6kKrZzIw2FeyXxcccbvTsKILItEECYmRNevo1mc5/f8BEXxIzEPhDpoKSTq +5JjWHCQH1shkwWyg2neL7g0UJ8nyV0pqqScE0L1WUZ1BHnVJAmGmR61WXxA3xCFz +JHSc2enRAgMBAAECggEAJVX2A1Z64x7hzAYzAHNfqZo2qu0zVbUvVPrHNkJ9XX6U +Jokt0zy/NC44Kp79aU6iR+p2UIVZf0bFF/CCUt6+TXPd3j3pZu1s8rElekAQNXwK +xfcEZ+AmkypaG9JJB7q5j5tGf1Zi8PN++OLtt3W95pmB/PyrI/JlE8KNqCV+BEnq +jLheACmehK+G7Rtez128lPvWHAnUTuQQ0wql1z4Z9VB5UwCYD3AxDz34jd8lwZQ1 +RQLUQblN46zpzkBTAX7sTmi9/y0nHJ7rJukTKxDciZ0xPkhtiAKjh6R2wb1TO51Q +fyGT7iyvtxnqQf+VoNYZGiQ/L7DMppSEHUMm0gkZuQKBgQDoFmLz5J7spQgASjXi +OLt8lWQOovzNC7K/pjILhD86o58efbZs6NdBrdq8GbeBtowd8HW0nwrxPbk0YN8W +Fr8kl6hAHYd4UYpMWYNDmB7KIVTAoU/Fk+p5AjXIBwQcYm9H66tDAO/yC8G8EEzu +iPoBTBQGMss87LH0jsSCDO0oQwKBgQC7xLY58zrU/cdK+ZbKmNA158CibH6ksXHP +Z4gm+yMW0t7Jdd39L+CfyAEWF9BAagJUuiaxIq3ZiHu7rA6PJ2G8jqRcIHyFgMRk +sxKTd7F86AI/IEZy7k0l//E4AsXERVgafvRuuSwYsm+ns6cuVYjAYRaHHinZpQao +Y98SxuxeWwKBgGFE+KX1XHIb3JWahKjSVCmrxuqnfsJFM95Evla7T3C5ILg7wdg1 +Yfoh7jnFoXZY1rK5k+tmeMSQtO1x6C2uzN9+PELa3Wsc6ZSEM5KBz+2xOH8fXHqX +Or8KoRW7cwqears+12FWpDnSmZjDUCrs97LRetb6NNnM7exsZYmH92FXAoGBAJDZ +fm4UCfWXVK+s/TuLSUvcXYmvQr9QN+j1CF5x7C7GO6GUcMzJq3H3e4cMldWrMeMk +u4Z4pz6iADnV0GF00vv/2iFL2mOu41J/pjvm4R/nZxxFjLNKzG8dE3vO/7uadw3x +lCT6al8e/+2SNM0UpOsrupI/na9NlGZArSyyElPzAoGBAIVv0H798SZjUxpfLT8s ++DI1QFbenNeoEaeXdkYtGrSPUhfZQQ2F744QDsbMm6+4oFkD9yg2A3DvSbd9+WrP +eDKKA5MAeNiD3X6glEcQOE1x6iTZ0jEXArv1n/qCl1qaUDPDUr8meIlkuwRgwyyW +vKxiQdtK+ZLUNfU2R5xZwo+X +-----END PRIVATE KEY----- diff --git a/pkg/helm/pkg/registry/testdata/tls/client.crt b/pkg/helm/pkg/registry/testdata/tls/client.crt index 5b1daf27..f54f46c7 100644 --- a/pkg/helm/pkg/registry/testdata/tls/client.crt +++ b/pkg/helm/pkg/registry/testdata/tls/client.crt @@ -1,20 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIDWzCCAkOgAwIBAgIUdJ6uRYm6RYesJ3CRoLokemFFgX8wDQYJKoZIhvcNAQEL -BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG -A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTIzMDYw -ODEwNTA0OFoXDTI0MDYwNzEwNTA0OFowWTELMAkGA1UEBhMCQ04xCzAJBgNVBAgM -AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEbMBkGA1UEAwwS -aGVsbS10ZXN0LXJlZ2lzdHJ5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAxuVrOJyfUO71wlqe/ae8pNVf3z+6b7aCYRrKJ4l66RKMPz9uP5lHD9QImCTU -LddER48iRr5nzaUKqNUsPn4tTcdaH9EEra+PDp+YeToyZARO+coxCq8yt1NxXrlb -E/q9Ie9QUlruhthrgr+5DC+qogZA8kcVPOs2+ObqeCCO6QGpECxROO2ysXHyjy2b -nwGCzZRz90M4z0ifXcey9RLzbmEsYymq6RbaeQvdzevgXhzIANktILuB0D3wJ2ae -WWP2CfBrjaPbOBtzdDhyl4T1aqLiUpDELUJLVpf/h6xCh52Q0svpsGVGtyO+npPe -kZ1LSVAnVGS6JlWWhs7RL0eaPwIDAQABoyEwHzAdBgNVHREEFjAUghJoZWxtLXRl -c3QtcmVnaXN0cnkwDQYJKoZIhvcNAQELBQADggEBABbxtODFOAeTJg4Q3SXqJ8Gq -zh3/1DaAEnMGHILYuS9tK5lisTLiUerqeQaHKR6U90HK/P1vVxe7PvwfHBrVsGkR -4YC6nivf8LMySKBQmsPUHjdotNZZ8O1pqd+CMqZe2ZuvzLZ4pPdw25lKjhZ7qI+t -hQ8yotiJALzEUWLJSgP5Y8k4hFfRGSso1oAC+WppQeW6ITqDo1MrzH7gpjnp+CJG -NWM1oAQCB1qIdo6gY386w6yLyUhfHtAVa3vviQ0dkRLiK95He5xZcO11rlDNdmgF -cF6lElkci8gPuH8UkKAT5bP9dAEbHPSjAIvg5O9NviknLiNAdFRKeTri+hqNLhE= +MIIDijCCAnKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJDTjEL +MAkGA1UECAwCR0QxCzAJBgNVBAcMAlNaMRMwEQYDVQQKDApBY21lLCBJbmMuMRUw +EwYDVQQDDAxBY21lIFJvb3QgQ0EwIBcNMjQwNDIxMTA1MzA1WhgPMzM5MzA0MDQx +MDUzMDVaMFkxCzAJBgNVBAYTAkNOMQswCQYDVQQIDAJHRDELMAkGA1UEBwwCU1ox +EzARBgNVBAoMCkFjbWUsIEluYy4xGzAZBgNVBAMMEmhlbG0tdGVzdC1yZWdpc3Ry +eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALK1aOjQvB337gmjkORj +QQyBDsScyWCnc1gjypcwPvi97+FFlp/jZUWasIa+FXeYWhwWiUI2tUttDNPZATqq +c2My1uME2Dm0PG9qAUuvW5CEdE7Bw3T2K/8A1myfux/vyMXEjXKHAl+uhTcqDlew +/yIF2gfO2dKYk+xnZwdE6w8bIQTqnaG0JxtK7Q0ULldsCOFtF+a4C9Zye6ggdieh +cwVuV41ehbVCK3E7AylTFwbALB6ZQ4z3V6jXrXBNdMKSLyesWAAwROcUB+S68NEa +5AWSfGXOT2glHzMHe7fJoulTetvJiaKBpxnFInMquBRzxpNO7A6eVmp6FQfpXqof +wikCAwEAAaNhMF8wHQYDVR0RBBYwFIISaGVsbS10ZXN0LXJlZ2lzdHJ5MB0GA1Ud +DgQWBBT6yXtjugflf08vGK3ClkHGw/D9HzAfBgNVHSMEGDAWgBREgz+BR+lJFNaG +2D7+tDVzzyjc4jANBgkqhkiG9w0BAQsFAAOCAQEAoDEJSYcegsEH1/mzAT8CUul5 +MkxF8U1Dtc8m6Nyosolh16AlJ5dmF5d537lqf0VwHDFtQiwexWVohTW9ngpk0C0Z +Jphf0+9ptpzBQn9x0mcHyKJRD3TbUc80oehY33bHAhPNdV3C1gwCfcbdX8Gz89ZT +MdLY0BfDELeBKVpaHd2vuK+E06X0a7T5P7vnYmNFpQOMyyytl7vM1TofmU905sNI +hrHqKH6c2G6QKW+vuiPoX+QbZFZ4NJ+Lco176wnpJjMZx3+Z6t4TV4sCaZgxj3RT +gDQBRnsD6m03ZoVZvIOlApUs3IEKXsqsrXJpuxfvU89u9z6vOn6TteFsExXiuA== -----END CERTIFICATE----- diff --git a/pkg/helm/pkg/registry/testdata/tls/client.key b/pkg/helm/pkg/registry/testdata/tls/client.key index 2f6a8aa1..3e764500 100644 --- a/pkg/helm/pkg/registry/testdata/tls/client.key +++ b/pkg/helm/pkg/registry/testdata/tls/client.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDG5Ws4nJ9Q7vXC -Wp79p7yk1V/fP7pvtoJhGsoniXrpEow/P24/mUcP1AiYJNQt10RHjyJGvmfNpQqo -1Sw+fi1Nx1of0QStr48On5h5OjJkBE75yjEKrzK3U3FeuVsT+r0h71BSWu6G2GuC -v7kML6qiBkDyRxU86zb45up4II7pAakQLFE47bKxcfKPLZufAYLNlHP3QzjPSJ9d -x7L1EvNuYSxjKarpFtp5C93N6+BeHMgA2S0gu4HQPfAnZp5ZY/YJ8GuNo9s4G3N0 -OHKXhPVqouJSkMQtQktWl/+HrEKHnZDSy+mwZUa3I76ek96RnUtJUCdUZLomVZaG -ztEvR5o/AgMBAAECggEBAKTaovRZXPOIHMrqsb0sun8lHEG+YJkXfRlfSw9aNDXa -2cPSn163fN7xr+3rGLKmKkHlsVNRnlgk46Dsj698hbBh+6FDbc1IJhrIzWgthHbB -23PO0rc4X6Dz2JParlLxELJ/2ONp2yqJVxMYNhiTqaqB5HLr1/6WNwo220CWO92D -vLz3rBHO5Vw5b5Y6Kt6MN6ciIHB2k+obhh4GQRJjUhvmmKCzbk1/R1PFYNwhhMN0 -Av6BdwFgngvNzJ8KMxGia7WJSvDYUk0++RRZ1esiZqwWRVCFFkm4Hj+gKJq6Xnz0 -a2nSvlC9k4GJvD9yY9VcDTJY+WsNN3Ny29gIFUeU9IECgYEA4norD3XakMthgOQk -3NE3HSvpZ22xtVgN9uN0b/JXbg7CLlYzn3tabpbQM/4uI6VG3Mk5Pk83QfKnr4W1 -aYO3YTEQ9B4g0eu3t4zfQOibY2+/Jb7Yfv/fH+pjkI26zYDQn61gsFdV9uxF7Pgu -NGNVe/eY+RkxEWsTtb40jcrbCgsCgYEA4NLWAdlrGKWZP5nLvM1hVB8r4WS82c0e -Orfyv2NhiqfRasARC1lQCqwbmCjb0c/eQiW7lJ7iSECc/8xW3HrJBYpG/tCxi9+m -SWxZXzRXDL8bmuoVvYeA/hFZayef5qCc8eiTYGQp6N5ozQHLXuPbNu7n6YSwvoU4 -ANrVBDRXxR0CgYEAmwbfhPS6iVT+yFjjNthrrqdJXQhElgrRfEfUg3DTEj4+A7P0 -IF4y1/KaUIzUjofrSuTfL1zQSW9OA6M2PCTymTAaF9CrzKZbGuTuSaMwAtASe0b5 -MW37EQDD6MZrsZJUvIjU38DY0m6Hqx9zmV7JvFMPPqxU30R5uHWbyderOmMCgYA5 -P3afIe3TaNeNCmyGtwWBli5mRnCQRVrdONnnQjckR3db52xvp15qWUjthfnzgyrl -TRZm0c5s94cC29WCbwGhF4Tcfee35ktBhwV66KkB5efxmonOqSJ/j4tlbcGZyGwu -bTqZ4OeLFJc7HKncj8jSRCNpoxAec22/SfnUCEARQQKBgAnwaN6kmGqIW2EsNOwB -DXCvG4HI9np5xN5Wo2dz7wqGtrt0TVtJ/PNBL3iadDLyPHahwoEVceFrQwqxjPsV -AoSwVDTdX96PKM/v/2ysw1JLf7UMT59mpxFoYiXCPn5Do4D1/25UfMOsJSmFo1Ij -Hkw1bqG8QneuME16BnDQfY3b +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCytWjo0Lwd9+4J +o5DkY0EMgQ7EnMlgp3NYI8qXMD74ve/hRZaf42VFmrCGvhV3mFocFolCNrVLbQzT +2QE6qnNjMtbjBNg5tDxvagFLr1uQhHROwcN09iv/ANZsn7sf78jFxI1yhwJfroU3 +Kg5XsP8iBdoHztnSmJPsZ2cHROsPGyEE6p2htCcbSu0NFC5XbAjhbRfmuAvWcnuo +IHYnoXMFbleNXoW1QitxOwMpUxcGwCwemUOM91eo161wTXTCki8nrFgAMETnFAfk +uvDRGuQFknxlzk9oJR8zB3u3yaLpU3rbyYmigacZxSJzKrgUc8aTTuwOnlZqehUH +6V6qH8IpAgMBAAECggEAFv5M3oG25pM3GyHiu2QC41k6nXT/2xIIfvtx7lR8kbQc +iGtT90QCjHtcAaY07GObmngS1oRj/K2uBBbsd9AlEwsgR2rg6EHGsd4dhw+rtBM6 +xMRdAfBHlmKU9Dp0EOag+kMxIN56oXV6ue+NE17YYNgIZs9ISvarN7RRNwf4x4NS +wpeWBqt120B3p9mGS64vE6wFxpRKSpFcpIp+yUswI45x8mbvCBr4tNW0OQ7y+WwS +rPp7GayutEUB9etRWviw10D7pz3HrxfarrZJm65IH1Fw5Ye6ayteoWg4IY2s3qSS +gh4qMZNMPeE6G3UBmkMdUf27+Udt8bSrSoz2Z8OlVQKBgQDcMY6h0BTFJcioBLhV +qe0FmckVNzs5jtzdwXFSjQduUCZ74ag5hsW3jQ0KNvd1B/xOv/Df6rYJY3ww8cQ1 ++KRTzt5B4qZwC1swuzqHWjR/W5XBlX3hRbs+I3imveaQ9zNFpktDZhaG72AWLLpa +Y31ddrkG4a8rTZFSuOVCbyj7JQKBgQDPxN/2Ayt/x+n/A4LNDSUQiUSALIeBHCCo +UzNQojcQLyobBVCIu5E3gRqIbvyRde7MQMGhfpLuaW7wmW0hqkUtRDYb4Hy52YMg +PFkno11wdpoEN3McLJNH08q+2dFjUKzQWygelDvkQMkwiL2syu+rEoUIEOCWyW6V +mPEPmfcdtQKBgEbqgwhkTrwr7hMG6iNUxex+2f9GOYHRHBsjeQ7gMtt5XtuZEqfs +WvNBr0hx6YK8nqryMG69VgFyFAZjZxEG0k3Xm0dW6sm9LpJkSnZbO/skkPe24MLT +xXk+zVXOZVqc8ttksmqzj1/H6odZwm7oCfE3EmI//z2QDtS4jcW2rVktAoGABfdn +Xw80PpUlGRemt/C6scDfYLbmpUSDg5HwFU6zOhnAocoDSAnq36crdeOKCTtTwjXR +2ati2MnaT7p4MdFL70LYMvC9ZDDk3RYekU7VrhcZ0Skuew6kpBlm5xgmNS3p6InV +mxsypRlfLa+fksi5HTaI73RcnrfmHxGnSoVnXUkCgYAHggM+T7e11OB+aEQ0nFcL +nS58M7QgB3/Xd7jGrl9Fi5qogtHE80epiV/srWaACZV6ricCZoDikOZzH1rRL2AA +Wlmb4j9yKp4P4uN0tniU0JuFEIQgLklAsEb4BG6izHI0UpXZTKVXY0XymOBdNtaw +QakjUJVKk+LqapUGIR8xRw== -----END PRIVATE KEY----- diff --git a/pkg/helm/pkg/registry/testdata/tls/server.crt b/pkg/helm/pkg/registry/testdata/tls/server.crt index 5fae09bb..42585e77 100644 --- a/pkg/helm/pkg/registry/testdata/tls/server.crt +++ b/pkg/helm/pkg/registry/testdata/tls/server.crt @@ -1,20 +1,21 @@ -----BEGIN CERTIFICATE----- -MIIDWzCCAkOgAwIBAgIUdJ6uRYm6RYesJ3CRoLokemFFgX4wDQYJKoZIhvcNAQEL -BQAwUzELMAkGA1UEBhMCQ04xCzAJBgNVBAgMAkdEMQswCQYDVQQHDAJTWjETMBEG -A1UECgwKQWNtZSwgSW5jLjEVMBMGA1UEAwwMQWNtZSBSb290IENBMB4XDTIzMDYw -ODEwNTAzM1oXDTI0MDYwNzEwNTAzM1owWTELMAkGA1UEBhMCQ04xCzAJBgNVBAgM -AkdEMQswCQYDVQQHDAJTWjETMBEGA1UECgwKQWNtZSwgSW5jLjEbMBkGA1UEAwwS -aGVsbS10ZXN0LXJlZ2lzdHJ5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEA59jg4ml82uyvrg+tXf/0S8WHuayl5fB3k1lIPtOrTt5KBNh6z5XHZDogsQ3m -UEko4gVUvKL0Einm1i5c3C6KFFj0RNib0QpOZtxu54mx2Rxazkge0yjoTMwl/P1o -pvRI6qfRri8LdlqWwU9wBIYmKqEM8jPjxKcCOaR0WyQmEJ6KbayTzsVNHaQxG/f3 -aIDCkp3tFl+LaTJHjGdZN7tvJsZ1wXlQy6gXTJIPXHDTS/uh3Xp8jgqhlnQPIr44 -HikiAp9DMnOBGO4u4cZjCr04cQnLS9knsBAQCjja9J9DnZ5vKatBHF3nOVAtGoBM -o69HcYoX5F10Qg8YOa7QwIYjpQIDAQABoyEwHzAdBgNVHREEFjAUghJoZWxtLXRl -c3QtcmVnaXN0cnkwDQYJKoZIhvcNAQELBQADggEBABMYICc/rzijGhFPFOeSrXyk -xFX9SSrGMl0CzV44sxzJFJ89BrW9bUWf4rLuc2ugqWp78kRKGMKgaytDrmGGuZKy -Qy+xl3DTAoc9FYOBphtcH1QndWdbpKSc2sTKvdeV6SslKwWXlAvcqIain80fWAkn -J+9Fd/rq3sJxCYsYhEf17pDjHDnG5ZUsBAWWzN+YjtSAe4PzT1KdljUPCC1GbF+H -1dx+MwapV+atftzlGjld8H73MXrKRNUSZM5lEFvzCZz48J1Ml6UVnYO+QCybeJtQ -lBT3/wclJ86e0eNkZJI0WTmrqlaNS/J7mbZ+4BhfjuO5PyZbLg8DcWmaKeNtT8M= +MIIDijCCAnKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJDTjEL +MAkGA1UECAwCR0QxCzAJBgNVBAcMAlNaMRMwEQYDVQQKDApBY21lLCBJbmMuMRUw +EwYDVQQDDAxBY21lIFJvb3QgQ0EwIBcNMjQwNDIxMTA1MzM4WhgPMzM5MzA0MDQx +MDUzMzhaMFkxCzAJBgNVBAYTAkNOMQswCQYDVQQIDAJHRDELMAkGA1UEBwwCU1ox +EzARBgNVBAoMCkFjbWUsIEluYy4xGzAZBgNVBAMMEmhlbG0tdGVzdC1yZWdpc3Ry +eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAME7cQl/08+JJv8aR07t +9nAnqQ6fYUwMBX8ULS2i6dXUoR0WpTBS8VgGUb2pNnH83r/VbvAcHSY/3LSUdt1d +j+cyCBQHXf8ySolInVP3L3s435WJuB9yzVZmlI8xrLOYmfVLnoyWjsirZT2KjLSw +gVgn0N9PQ6K+IvrIph/jgBsv9c6oCLvWH1TcVtS5AN6gb5aSvr2cXRCVelntLH9V +QpsmceMtHfzJUW37AarEvTj8NNTOWMIPNs1rqNpFEy1AepHy388C63SJuqy69dvx +9wE1DCCduH3PMgF7cxWicow9JcIK4kZLrBD4ULdSxTmqA1+yLf+VHhSrDIQy3Lwj +bBcCAwEAAaNhMF8wHQYDVR0RBBYwFIISaGVsbS10ZXN0LXJlZ2lzdHJ5MB0GA1Ud +DgQWBBSQliNnbB0bCKi3c3mqifj3CPZbxTAfBgNVHSMEGDAWgBREgz+BR+lJFNaG +2D7+tDVzzyjc4jANBgkqhkiG9w0BAQsFAAOCAQEAPztylxowZuLT3zRdB0JHkmnI +zoUmG1hwBeRtruMqQGZnSX0F2glTVKcJzC+Wl5XzMHt2AcRmYl4qk7flWfFavlFp +7ycIbbKH/4MVmuJF53Zy40fOZ2rDSfyjNsPNQLxTg3tlWVbEAcuyKAWLJ5RZG+hL +fSKVFzdEsV+Ux//BUuce/q42hTBbZF09GtG+Lg7/DgxGIY7CLzID8GfdcYRBv4sX +eeOHeGnDC1zttMcnWU49zghJ8MXwo7tOsybQEZmSZZdwQwm+pEwxdibJAXQ/OSGb +c7RI+clTmnwbP/vnig5RnMALFbUaP2aE/mTMYLWBBV1VqWkfx4Xc7xbE9lrpuA== -----END CERTIFICATE----- diff --git a/pkg/helm/pkg/registry/testdata/tls/server.key b/pkg/helm/pkg/registry/testdata/tls/server.key index da44121a..4f7bd54f 100644 --- a/pkg/helm/pkg/registry/testdata/tls/server.key +++ b/pkg/helm/pkg/registry/testdata/tls/server.key @@ -1,28 +1,28 @@ -----BEGIN PRIVATE KEY----- -MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDn2ODiaXza7K+u -D61d//RLxYe5rKXl8HeTWUg+06tO3koE2HrPlcdkOiCxDeZQSSjiBVS8ovQSKebW -LlzcLooUWPRE2JvRCk5m3G7nibHZHFrOSB7TKOhMzCX8/Wim9Ejqp9GuLwt2WpbB -T3AEhiYqoQzyM+PEpwI5pHRbJCYQnoptrJPOxU0dpDEb9/dogMKSne0WX4tpMkeM -Z1k3u28mxnXBeVDLqBdMkg9ccNNL+6HdenyOCqGWdA8ivjgeKSICn0Myc4EY7i7h -xmMKvThxCctL2SewEBAKONr0n0Odnm8pq0EcXec5UC0agEyjr0dxihfkXXRCDxg5 -rtDAhiOlAgMBAAECggEBAJ6kfFzwqYpz4lJMT+i+Nz+RzilyxaHtRSUCNrkmxVWW -LTfbmU1pw6IFVFFSnYHaTas60pyxNCkpmtZ7qvbOsZTyuVJSlWwYjUU9GHY+df+F -s2zrVIxQtYO3PVc7Xty+0xYd9xAlCMbXfciQvqmZ0Yvh36Xrc7MgRBmFOkkTFyjO -xaT70D5jwK0QKU8sMY+b9XvvaX59jbRmYAHL0wNcke/E7J4NKEAYfRI+x7kuFhP4 -yDbs9YE0u51cHYAGV4EujZhnv2AwvDnAWs0yHqIbVOIWI9+JRYKmPScr7b1bJfd/ -yy24GXvBu7Ss4TkfsJ/FdGXESr0Gj0ZIPIneDn/vrQECgYEA9jHu4FjTbRff+4tV -3zJJe88+yByjC6Hhj223JmRpCXQrXl2WLAYXl94p7M5NFdkD5QG7jsNUogLb73dV -ekUjuQl7IhJZYcRAXcnlkF+8pKt1duA0uRa22VtlR2wyn8oSnLV/9088Moh35sCP -MjWQDlZ/BW7YUPrOtB14eUCvMjECgYEA8RSpmXZVQdGnIIm6gC3rEhtfHQqAoBn0 -JRvnRXC/LKeVSgVF3ijeT9P/0JQuM9uxubV314nY+fhXsM5kkMZUoXMMSoxE+xPw -cgArpzwsleMn7BQ/UF3GLpdkUgNFI8bolZFbIa54F7YSFNto0NBp3mkceCJwoWmZ -BPIoo4zpV7UCgYEAviK2L8GqF5jWvPhRK300z0+xVu725ObywsijKB1oGYsEa26v -qfRSiFFl46M4WWUu4tBBv/IPDMhUf06UT0fSXPd7h0bQjPb6FvT0PFoT4MEiiNqD -HWbzdE5nm49uUYXIdgqed6tT/Fr07ttMPCStysT2eIWwvmnU9bnE7zALniECgYAr -HM7XqtnEU4HXx8macpu/OTXhM6ec+gc3O644NNl7WtzPx/GesSBQllEBM/6vN3Kp -C1LLMNOkoEzOSZqiaVVpKfHgwwTzAbXWLUGhPpmalGznQxevf5WZb2l5YSxUIZYm -aUAq3dCMLPs+z54G+b51D8cPlNkfhIrg34108hYooQKBgQDWMbc6wY6frvJCmesx -i7F/JHJweqcQdW649RCvtK8M/O062/3vvSNTxqEjPaJOGiD4Cn+D5pYchVujqlTM -8DK77N97NzQvpHm81lpKVIg5sObarvT3RnCSRpOumbX5SCBoBUs+nVC01/zZz79c -AJFLAeHI1RjhB0AFpRDCvZZk6w== +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDBO3EJf9PPiSb/ +GkdO7fZwJ6kOn2FMDAV/FC0tounV1KEdFqUwUvFYBlG9qTZx/N6/1W7wHB0mP9y0 +lHbdXY/nMggUB13/MkqJSJ1T9y97ON+Vibgfcs1WZpSPMayzmJn1S56Mlo7Iq2U9 +ioy0sIFYJ9DfT0OiviL6yKYf44AbL/XOqAi71h9U3FbUuQDeoG+Wkr69nF0QlXpZ +7Sx/VUKbJnHjLR38yVFt+wGqxL04/DTUzljCDzbNa6jaRRMtQHqR8t/PAut0ibqs +uvXb8fcBNQwgnbh9zzIBe3MVonKMPSXCCuJGS6wQ+FC3UsU5qgNfsi3/lR4UqwyE +Mty8I2wXAgMBAAECggEAAKk5/ytSlGCTicatCcZJbb0xy3ZpUcyuVCH28ABuEyiY +DugEU3PLll6Aw+JWG/Ieg1xKj3dSwWe+H785eazK3W9pYanCY4+1FSuMOW/pPkWs +IvA536ARhCmNRo27JoSJU+Wyh1tlTHOk2mukt/vs/vOb6x4NTPttIs7lUP42DC6O +e/gTvwD13Rrg9PC0aDpZzLqdmXyUoHQ4h8dfYytDE9rZ1gC2CNdd7NWvt2JUppRx +qWR5OQxm+QiZqrMDUFTZISB/bD7MX/Ubq5InAfwdznzyav4uWsxq72FuoFFGl9xh +l6WEdusyKay/eNZgXqrHyuJvmt1PUL+Azu8ZYD+C2QKBgQD/nogcrVKLzmmrnggG +lMAvF5tp3gMI7+wqALH/79Gelvj5CWzGBnS7BcuXFR5cbpLk1cW6mj16IPIRA2CR +xpGfYKtYt0j5hvIZTg3TpK3Pj/kqEv0AicdGP6SYduJYgaUwFKRzHSR+N3121v5X +MVXKb5q6pD1wb7cOc2FJAOySHQKBgQDBhR8bAg99EgvVNioSkot++kRffWxwZ9uS +k1jmhLl7djb1tND4yZGZmi8+bdw7qz7J5yEJHuJiMwOkDsBokpKykk36tjBx3UiV +Z46OiKbRkiwBLg6fio6BVwAuQpoQ+qMWwkjZFPzWiEhxTPo3ZyiJP8JlT8sG3rV4 +My3wvLagwwKBgFT3RRcDJaUC/2zkIpbNavQ8TJRsD2YxGbb8dC42cN7eH/Pnhhhs +nPBthLa7dlQTDRCzXf4gtr6ZpNyy2q6Z6l2nrEzY35DRojd3EnF/E6cinBe4KBC9 +u1dGYFetbJ8uuNG6is8YqMCrgTC3VeN1qqaXYj8XyLRO7fIHuBakD/6hAoGARDal +cUK3rPF4hE5UZDmNvFOBWFuAptqlFjSkKJVuQCu6Ub/LzXZXwVoM/yeAcvP47Phw +t6NQTycGSIT+o53O4e0aWZ5w0yIaHLflEy7uBn9MzZmrg+c2NjcxlBzb69I9PJ99 +SC/Ss9hUGMP2iyLssfxsjIOk4CYOt3Dq56nNgjsCgYBWOLVMCV10DpYKUY5LFq60 +CJppqPyBfGB+5LLYfOp8JSIh1ZwSL139A2oCynGjrIyyPksdkBUMcS/qLhT1vmzo +zdUZMwK8D/TjF037F/t34LUHweP/2pl90DUcNPHJJs/IhXji7Kpdnqf3LhSXmgNs +d7TshLFRKM1z2BlZPZ56cA== -----END PRIVATE KEY----- diff --git a/pkg/helm/pkg/registry/transport.go b/pkg/helm/pkg/registry/transport.go new file mode 100644 index 00000000..f039a815 --- /dev/null +++ b/pkg/helm/pkg/registry/transport.go @@ -0,0 +1,175 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "bytes" + "fmt" + "io" + "log/slog" + "mime" + "net/http" + "strings" + "sync/atomic" + + "oras.land/oras-go/v2/registry/remote/retry" +) + +var ( + // requestCount records the number of logged request-response pairs and will + // be used as the unique id for the next pair. + requestCount atomic.Uint64 + + // toScrub is a set of headers that should be scrubbed from the log. + toScrub = []string{ + "Authorization", + "Set-Cookie", + } +) + +// payloadSizeLimit limits the maximum size of the response body to be printed. +const payloadSizeLimit int64 = 16 * 1024 // 16 KiB + +// LoggingTransport is an http.RoundTripper that keeps track of the in-flight +// request and add hooks to report HTTP tracing events. +type LoggingTransport struct { + http.RoundTripper +} + +// NewTransport creates and returns a new instance of LoggingTransport +func NewTransport(debug bool) *retry.Transport { + type cloner[T any] interface { + Clone() T + } + + // try to copy (clone) the http.DefaultTransport so any mutations we + // perform on it (e.g. TLS config) are not reflected globally + // follow https://github.com/golang/go/issues/39299 for a more elegant + // solution in the future + transport := http.DefaultTransport + if t, ok := transport.(cloner[*http.Transport]); ok { + transport = t.Clone() + } else if t, ok := transport.(cloner[http.RoundTripper]); ok { + // this branch will not be used with go 1.20, it was added + // optimistically to try to clone if the http.DefaultTransport + // implementation changes, still the Clone method in that case + // might not return http.RoundTripper... + transport = t.Clone() + } + if debug { + transport = &LoggingTransport{RoundTripper: transport} + } + + return retry.NewTransport(transport) +} + +// RoundTrip calls base round trip while keeping track of the current request. +func (t *LoggingTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) { + id := requestCount.Add(1) - 1 + + slog.Debug(req.Method, "id", id, "url", req.URL, "header", logHeader(req.Header)) + resp, err = t.RoundTripper.RoundTrip(req) + if err != nil { + slog.Debug("Response"[:len(req.Method)], "id", id, "error", err) + } else if resp != nil { + slog.Debug("Response"[:len(req.Method)], "id", id, "status", resp.Status, "header", logHeader(resp.Header), "body", logResponseBody(resp)) + } else { + slog.Debug("Response"[:len(req.Method)], "id", id, "response", "nil") + } + + return resp, err +} + +// logHeader prints out the provided header keys and values, with auth header scrubbed. +func logHeader(header http.Header) string { + if len(header) > 0 { + var headers []string + for k, v := range header { + for _, h := range toScrub { + if strings.EqualFold(k, h) { + v = []string{"*****"} + } + } + headers = append(headers, fmt.Sprintf(" %q: %q", k, strings.Join(v, ", "))) + } + return strings.Join(headers, "\n") + } + return " Empty header" +} + +// logResponseBody prints out the response body if it is printable and within size limit. +func logResponseBody(resp *http.Response) string { + if resp.Body == nil || resp.Body == http.NoBody { + return " No response body to print" + } + + // non-applicable body is not printed and remains untouched for subsequent processing + contentType := resp.Header.Get("Content-Type") + if contentType == "" { + return " Response body without a content type is not printed" + } + if !isPrintableContentType(contentType) { + return fmt.Sprintf(" Response body of content type %q is not printed", contentType) + } + + buf := bytes.NewBuffer(nil) + body := resp.Body + // restore the body by concatenating the read body with the remaining body + resp.Body = struct { + io.Reader + io.Closer + }{ + Reader: io.MultiReader(buf, body), + Closer: body, + } + // read the body up to limit+1 to check if the body exceeds the limit + if _, err := io.CopyN(buf, body, payloadSizeLimit+1); err != nil && err != io.EOF { + return fmt.Sprintf(" Error reading response body: %v", err) + } + + readBody := buf.String() + if len(readBody) == 0 { + return " Response body is empty" + } + if containsCredentials(readBody) { + return " Response body redacted due to potential credentials" + } + if len(readBody) > int(payloadSizeLimit) { + return readBody[:payloadSizeLimit] + "\n...(truncated)" + } + return readBody +} + +// isPrintableContentType returns true if the contentType is printable. +func isPrintableContentType(contentType string) bool { + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + return false + } + + switch mediaType { + case "application/json", // JSON types + "text/plain", "text/html": // text types + return true + } + return strings.HasSuffix(mediaType, "+json") +} + +// containsCredentials returns true if the body contains potential credentials. +func containsCredentials(body string) bool { + return strings.Contains(body, `"token"`) || strings.Contains(body, `"access_token"`) +} diff --git a/pkg/helm/pkg/registry/transport_test.go b/pkg/helm/pkg/registry/transport_test.go new file mode 100644 index 00000000..b4990c52 --- /dev/null +++ b/pkg/helm/pkg/registry/transport_test.go @@ -0,0 +1,399 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "bytes" + "errors" + "io" + "net/http" + "testing" +) + +var errMockRead = errors.New("mock read error") + +type errorReader struct{} + +func (e *errorReader) Read(_ []byte) (n int, err error) { + return 0, errMockRead +} + +func Test_isPrintableContentType(t *testing.T) { + tests := []struct { + name string + contentType string + want bool + }{ + { + name: "Empty content type", + contentType: "", + want: false, + }, + { + name: "General JSON type", + contentType: "application/json", + want: true, + }, + { + name: "General JSON type with charset", + contentType: "application/json; charset=utf-8", + want: true, + }, + { + name: "Random type with application/json prefix", + contentType: "application/jsonwhatever", + want: false, + }, + { + name: "Manifest type in JSON", + contentType: "application/vnd.oci.image.manifest.v1+json", + want: true, + }, + { + name: "Manifest type in JSON with charset", + contentType: "application/vnd.oci.image.manifest.v1+json; charset=utf-8", + want: true, + }, + { + name: "Random content type in JSON", + contentType: "application/whatever+json", + want: true, + }, + { + name: "Plain text type", + contentType: "text/plain", + want: true, + }, + { + name: "Plain text type with charset", + contentType: "text/plain; charset=utf-8", + want: true, + }, + { + name: "Random type with text/plain prefix", + contentType: "text/plainnnnn", + want: false, + }, + { + name: "HTML type", + contentType: "text/html", + want: true, + }, + { + name: "Plain text type with charset", + contentType: "text/html; charset=utf-8", + want: true, + }, + { + name: "Random type with text/html prefix", + contentType: "text/htmlllll", + want: false, + }, + { + name: "Binary type", + contentType: "application/octet-stream", + want: false, + }, + { + name: "Unknown type", + contentType: "unknown/unknown", + want: false, + }, + { + name: "Invalid type", + contentType: "text/", + want: false, + }, + { + name: "Random string", + contentType: "random123!@#", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isPrintableContentType(tt.contentType); got != tt.want { + t.Errorf("isPrintableContentType() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_logResponseBody(t *testing.T) { + tests := []struct { + name string + resp *http.Response + want string + wantData []byte + }{ + { + name: "Nil body", + resp: &http.Response{ + Body: nil, + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + want: " No response body to print", + }, + { + name: "No body", + wantData: nil, + resp: &http.Response{ + Body: http.NoBody, + ContentLength: 100, // in case of HEAD response, the content length is set but the body is empty + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + want: " No response body to print", + }, + { + name: "Empty body", + wantData: []byte(""), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte(""))), + ContentLength: 0, + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: " Response body is empty", + }, + { + name: "Unknown content length", + wantData: []byte("whatever"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("whatever"))), + ContentLength: -1, + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: "whatever", + }, + { + name: "Missing content type header", + wantData: []byte("whatever"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("whatever"))), + ContentLength: 8, + }, + want: " Response body without a content type is not printed", + }, + { + name: "Empty content type header", + wantData: []byte("whatever"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("whatever"))), + ContentLength: 8, + Header: http.Header{"Content-Type": []string{""}}, + }, + want: " Response body without a content type is not printed", + }, + { + name: "Non-printable content type", + wantData: []byte("binary data"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("binary data"))), + ContentLength: 11, + Header: http.Header{"Content-Type": []string{"application/octet-stream"}}, + }, + want: " Response body of content type \"application/octet-stream\" is not printed", + }, + { + name: "Body at the limit", + wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit)), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit)))), + ContentLength: payloadSizeLimit, + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))), + }, + { + name: "Body larger than limit", + wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)))), // 1 byte larger than limit + ContentLength: payloadSizeLimit + 1, + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))) + "\n...(truncated)", + }, + { + name: "Printable content type within limit", + wantData: []byte("data"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("data"))), + ContentLength: 4, + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: "data", + }, + { + name: "Actual body size is larger than content length", + wantData: []byte("data"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("data"))), + ContentLength: 3, // mismatched content length + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: "data", + }, + { + name: "Actual body size is larger than content length and exceeds limit", + wantData: bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader(bytes.Repeat([]byte("a"), int(payloadSizeLimit+1)))), // 1 byte larger than limit + ContentLength: 1, // mismatched content length + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: string(bytes.Repeat([]byte("a"), int(payloadSizeLimit))) + "\n...(truncated)", + }, + { + name: "Actual body size is smaller than content length", + wantData: []byte("data"), + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte("data"))), + ContentLength: 5, // mismatched content length + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: "data", + }, + { + name: "Body contains token", + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte(`{"token":"12345"}`))), + ContentLength: 17, + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + wantData: []byte(`{"token":"12345"}`), + want: " Response body redacted due to potential credentials", + }, + { + name: "Body contains access_token", + resp: &http.Response{ + Body: io.NopCloser(bytes.NewReader([]byte(`{"access_token":"12345"}`))), + ContentLength: 17, + Header: http.Header{"Content-Type": []string{"application/json"}}, + }, + wantData: []byte(`{"access_token":"12345"}`), + want: " Response body redacted due to potential credentials", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := logResponseBody(tt.resp); got != tt.want { + t.Errorf("logResponseBody() = %v, want %v", got, tt.want) + } + // validate the response body + if tt.resp.Body != nil { + readBytes, err := io.ReadAll(tt.resp.Body) + if err != nil { + t.Errorf("failed to read body after logResponseBody(), err= %v", err) + } + if !bytes.Equal(readBytes, tt.wantData) { + t.Errorf("resp.Body after logResponseBody() = %v, want %v", readBytes, tt.wantData) + } + if closeErr := tt.resp.Body.Close(); closeErr != nil { + t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr) + } + } + }) + } +} + +func Test_logResponseBody_error(t *testing.T) { + tests := []struct { + name string + resp *http.Response + want string + }{ + { + name: "Error reading body", + resp: &http.Response{ + Body: io.NopCloser(&errorReader{}), + ContentLength: 10, + Header: http.Header{"Content-Type": []string{"text/plain"}}, + }, + want: " Error reading response body: mock read error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := logResponseBody(tt.resp); got != tt.want { + t.Errorf("logResponseBody() = %v, want %v", got, tt.want) + } + if closeErr := tt.resp.Body.Close(); closeErr != nil { + t.Errorf("failed to close body after logResponseBody(), err= %v", closeErr) + } + }) + } +} + +func Test_containsCredentials(t *testing.T) { + tests := []struct { + name string + body string + want bool + }{ + { + name: "Contains token keyword", + body: `{"token": "12345"}`, + want: true, + }, + { + name: "Contains quoted token keyword", + body: `whatever "token" blah`, + want: true, + }, + { + name: "Contains unquoted token keyword", + body: `whatever token blah`, + want: false, + }, + { + name: "Contains access_token keyword", + body: `{"access_token": "12345"}`, + want: true, + }, + { + name: "Contains quoted access_token keyword", + body: `whatever "access_token" blah`, + want: true, + }, + { + name: "Contains unquoted access_token keyword", + body: `whatever access_token blah`, + want: false, + }, + { + name: "Does not contain credentials", + body: `{"key": "value"}`, + want: false, + }, + { + name: "Empty body", + body: ``, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := containsCredentials(tt.body); got != tt.want { + t.Errorf("containsCredentials() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/helm/pkg/registry/util.go b/pkg/helm/pkg/registry/util.go deleted file mode 100644 index ca7b07bb..00000000 --- a/pkg/helm/pkg/registry/util.go +++ /dev/null @@ -1,248 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package registry // import "helm.sh/helm/v3/pkg/registry" - -import ( - "bytes" - "context" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/Masterminds/semver/v3" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - orascontext "oras.land/oras-go/pkg/context" - "oras.land/oras-go/pkg/registry" - - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" - - "github.com/werf/nelm/pkg/helm/intern/tlsutil" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" -) - -var immutableOciAnnotations = []string{ - ocispec.AnnotationVersion, - ocispec.AnnotationTitle, -} - -// IsOCI determines whether or not a URL is to be treated as an OCI URL -func IsOCI(url string) bool { - return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) -} - -// ContainsTag determines whether a tag is found in a provided list of tags -func ContainsTag(tags []string, tag string) bool { - for _, t := range tags { - if tag == t { - return true - } - } - return false -} - -func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { - var constraint *semver.Constraints - if versionString == "" { - // If string is empty, set wildcard constraint - constraint, _ = semver.NewConstraint("*") - } else { - // when customer input exact version, check whether have exact match - // one first - for _, v := range tags { - if versionString == v { - return v, nil - } - } - - // Otherwise set constraint to the string given - var err error - constraint, err = semver.NewConstraint(versionString) - if err != nil { - return "", err - } - } - - // Otherwise try to find the first available version matching the string, - // in case it is a constraint - for _, v := range tags { - test, err := semver.NewVersion(v) - if err != nil { - continue - } - if constraint.Check(test) { - return v, nil - } - } - - return "", errors.Errorf("Could not locate a version matching provided version string %s", versionString) -} - -// extractChartMeta is used to extract a chart metadata from a byte array -func extractChartMeta(chartData []byte, opts helmopts.HelmOptions) (*chart.Metadata, error) { - ch, err := loader.LoadArchive(bytes.NewReader(chartData), opts) - if err != nil { - return nil, err - } - return ch.Metadata, nil -} - -// ctx retrieves a fresh context. -// disable verbose logging coming from ORAS (unless debug is enabled) -func ctx(out io.Writer, debug bool) context.Context { - if !debug { - return orascontext.Background() - } - ctx := orascontext.WithLoggerFromWriter(context.Background(), out) - orascontext.GetLogger(ctx).Logger.SetLevel(logrus.DebugLevel) - return ctx -} - -// parseReference will parse and validate the reference, and clean tags when -// applicable tags are only cleaned when plus (+) signs are present, and are -// converted to underscores (_) before pushing -// See https://github.com/helm/helm/issues/10166 -func parseReference(raw string) (registry.Reference, error) { - // The sole possible reference modification is replacing plus (+) signs - // present in tags with underscores (_). To do this properly, we first - // need to identify a tag, and then pass it on to the reference parser - // NOTE: Passing immediately to the reference parser will fail since (+) - // signs are an invalid tag character, and simply replacing all plus (+) - // occurrences could invalidate other portions of the URI - parts := strings.Split(raw, ":") - if len(parts) > 1 && !strings.Contains(parts[len(parts)-1], "/") { - tag := parts[len(parts)-1] - - if tag != "" { - // Replace any plus (+) signs with known underscore (_) conversion - newTag := strings.ReplaceAll(tag, "+", "_") - raw = strings.ReplaceAll(raw, tag, newTag) - } - } - - return registry.ParseReference(raw) -} - -// NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled. -func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) { - tlsConf, err := tlsutil.NewClientTLS(certFile, keyFile, caFile, insecureSkipTLSverify) - if err != nil { - return nil, fmt.Errorf("can't create TLS config for client: %s", err) - } - // Create a new registry client - registryClient, err := NewClient( - ClientOptDebug(debug), - ClientOptEnableCache(true), - ClientOptWriter(out), - ClientOptCredentialsFile(registryConfig), - ClientOptHTTPClient(&http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConf, - }, - }), - ) - if err != nil { - return nil, err - } - return registryClient, nil -} - -// generateOCIAnnotations will generate OCI annotations to include within the OCI manifest -func generateOCIAnnotations(meta *chart.Metadata, test bool) map[string]string { - - // Get annotations from Chart attributes - ociAnnotations := generateChartOCIAnnotations(meta, test) - - // Copy Chart annotations -annotations: - for chartAnnotationKey, chartAnnotationValue := range meta.Annotations { - - // Avoid overriding key properties - for _, immutableOciKey := range immutableOciAnnotations { - if immutableOciKey == chartAnnotationKey { - continue annotations - } - } - - // Add chart annotation - ociAnnotations[chartAnnotationKey] = chartAnnotationValue - } - - return ociAnnotations -} - -// getChartOCIAnnotations will generate OCI annotations from the provided chart -func generateChartOCIAnnotations(meta *chart.Metadata, test bool) map[string]string { - chartOCIAnnotations := map[string]string{} - - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationDescription, meta.Description) - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationTitle, meta.Name) - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationVersion, meta.Version) - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) - - if !test { - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, helmtime.Now().UTC().Format(time.RFC3339)) - } - - if len(meta.Sources) > 0 { - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationSource, meta.Sources[0]) - } - - if meta.Maintainers != nil && len(meta.Maintainers) > 0 { - var maintainerSb strings.Builder - - for maintainerIdx, maintainer := range meta.Maintainers { - - if len(maintainer.Name) > 0 { - maintainerSb.WriteString(maintainer.Name) - } - - if len(maintainer.Email) > 0 { - maintainerSb.WriteString(" (") - maintainerSb.WriteString(maintainer.Email) - maintainerSb.WriteString(")") - } - - if maintainerIdx < len(meta.Maintainers)-1 { - maintainerSb.WriteString(", ") - } - - } - - chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationAuthors, maintainerSb.String()) - - } - - return chartOCIAnnotations -} - -// addToMap takes an existing map and adds an item if the value is not empty -func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string { - - // Add item to map if its - if len(strings.TrimSpace(newValue)) > 0 { - inputMap[newKey] = newValue - } - - return inputMap - -} diff --git a/pkg/helm/pkg/registry/util_test.go b/pkg/helm/pkg/registry/util_test.go deleted file mode 100644 index 31f5aae9..00000000 --- a/pkg/helm/pkg/registry/util_test.go +++ /dev/null @@ -1,240 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package registry // import "helm.sh/helm/v3/pkg/registry" - -import ( - "reflect" - "testing" - "time" - - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" -) - -func TestGenerateOCIChartAnnotations(t *testing.T) { - - tests := []struct { - name string - chart *chart.Metadata - expect map[string]string - }{ - { - "Baseline chart", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - }, - }, - { - "Simple chart values", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Home: "https://helm.sh", - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "org.opencontainers.image.url": "https://helm.sh", - }, - }, - { - "Maintainer without email", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Home: "https://helm.sh", - Maintainers: []*chart.Maintainer{ - { - Name: "John Snow", - }, - }, - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "org.opencontainers.image.url": "https://helm.sh", - "org.opencontainers.image.authors": "John Snow", - }, - }, - { - "Maintainer with email", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Home: "https://helm.sh", - Maintainers: []*chart.Maintainer{ - {Name: "John Snow", Email: "john@winterfell.com"}, - }, - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "org.opencontainers.image.url": "https://helm.sh", - "org.opencontainers.image.authors": "John Snow (john@winterfell.com)", - }, - }, - { - "Multiple Maintainers", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Home: "https://helm.sh", - Maintainers: []*chart.Maintainer{ - {Name: "John Snow", Email: "john@winterfell.com"}, - {Name: "Jane Snow"}, - }, - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "org.opencontainers.image.url": "https://helm.sh", - "org.opencontainers.image.authors": "John Snow (john@winterfell.com), Jane Snow", - }, - }, - { - "Chart with Sources", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Sources: []string{ - "https://github.com/helm/helm", - }, - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "org.opencontainers.image.source": "https://github.com/helm/helm", - }, - }, - } - - for _, tt := range tests { - - result := generateChartOCIAnnotations(tt.chart, true) - - if !reflect.DeepEqual(tt.expect, result) { - t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) - } - - } -} - -func TestGenerateOCIAnnotations(t *testing.T) { - - tests := []struct { - name string - chart *chart.Metadata - expect map[string]string - }{ - { - "Baseline chart", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - }, - }, - { - "Simple chart values with custom Annotations", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Annotations: map[string]string{ - "extrakey": "extravlue", - "anotherkey": "anothervalue", - }, - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "extrakey": "extravlue", - "anotherkey": "anothervalue", - }, - }, - { - "Verify Chart Name and Version cannot be overridden from annotations", - &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - Description: "OCI Helm Chart", - Annotations: map[string]string{ - "org.opencontainers.image.title": "badchartname", - "org.opencontainers.image.version": "1.0.0", - "extrakey": "extravlue", - }, - }, - map[string]string{ - "org.opencontainers.image.title": "oci", - "org.opencontainers.image.version": "0.0.1", - "org.opencontainers.image.description": "OCI Helm Chart", - "extrakey": "extravlue", - }, - }, - } - - for _, tt := range tests { - - result := generateOCIAnnotations(tt.chart, true) - - if !reflect.DeepEqual(tt.expect, result) { - t.Errorf("%s: expected map %v, got %v", tt.name, tt.expect, result) - } - - } -} - -func TestGenerateOCICreatedAnnotations(t *testing.T) { - chart := &chart.Metadata{ - Name: "oci", - Version: "0.0.1", - } - - result := generateOCIAnnotations(chart, false) - - // Check that created annotation exists - if _, ok := result[ocispec.AnnotationCreated]; !ok { - t.Errorf("%s annotation not created", ocispec.AnnotationCreated) - } - - // Verify value of created artifact in RFC3339 format - if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { - t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) - } - -} diff --git a/pkg/helm/pkg/registry/utils_test.go b/pkg/helm/pkg/registry/utils_test.go deleted file mode 100644 index 552986cb..00000000 --- a/pkg/helm/pkg/registry/utils_test.go +++ /dev/null @@ -1,393 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package registry - -import ( - "bytes" - "context" - "crypto/tls" - "fmt" - "io" - "net" - "net/http" - "net/http/httptest" - "net/url" - "os" - "path/filepath" - "strings" - "time" - - "github.com/distribution/distribution/v3/configuration" - "github.com/distribution/distribution/v3/registry" - _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" - _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" - "github.com/foxcpp/go-mockdns" - "github.com/phayes/freeport" - "github.com/stretchr/testify/suite" - "golang.org/x/crypto/bcrypt" - - "github.com/werf/nelm/pkg/helm/intern/tlsutil" -) - -const ( - tlsServerKey = "./testdata/tls/server.key" - tlsServerCert = "./testdata/tls/server.crt" - tlsCA = "./testdata/tls/ca.crt" - tlsKey = "./testdata/tls/client.key" - tlsCert = "./testdata/tls/client.crt" -) - -var ( - testWorkspaceDir = "helm-registry-test" - testHtpasswdFileBasename = "authtest.htpasswd" - testUsername = "myuser" - testPassword = "mypass" -) - -type TestSuite struct { - suite.Suite - Out io.Writer - DockerRegistryHost string - CompromisedRegistryHost string - WorkspaceDir string - RegistryClient *Client - - // A mock DNS server needed for TLS connection testing. - srv *mockdns.Server -} - -func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { - suite.WorkspaceDir = testWorkspaceDir - os.RemoveAll(suite.WorkspaceDir) - os.Mkdir(suite.WorkspaceDir, 0700) - - var ( - out bytes.Buffer - err error - ) - suite.Out = &out - credentialsFile := filepath.Join(suite.WorkspaceDir, CredentialsFileBasename) - - // init test client - opts := []ClientOption{ - ClientOptDebug(true), - ClientOptEnableCache(true), - ClientOptWriter(suite.Out), - ClientOptCredentialsFile(credentialsFile), - ClientOptResolver(nil), - } - - if tlsEnabled { - var tlsConf *tls.Config - if insecure { - tlsConf, err = tlsutil.NewClientTLS("", "", "", true) - } else { - tlsConf, err = tlsutil.NewClientTLS(tlsCert, tlsKey, tlsCA, false) - } - httpClient := &http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConf, - }, - } - suite.Nil(err, "no error loading tls config") - opts = append(opts, ClientOptHTTPClient(httpClient)) - } else { - opts = append(opts, ClientOptPlainHTTP()) - } - - suite.RegistryClient, err = NewClient(opts...) - suite.Nil(err, "no error creating registry client") - - // create htpasswd file (w BCrypt, which is required) - pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) - suite.Nil(err, "no error generating bcrypt password for test htpasswd file") - htpasswdPath := filepath.Join(suite.WorkspaceDir, testHtpasswdFileBasename) - err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) - suite.Nil(err, "no error creating test htpasswd file") - - // Registry config - config := &configuration.Configuration{} - port, err := freeport.GetFreePort() - suite.Nil(err, "no error finding free port for test registry") - - // Change the registry host to another host which is not localhost. - // This is required because Docker enforces HTTP if the registry - // host is localhost/127.0.0.1. - suite.DockerRegistryHost = fmt.Sprintf("helm-test-registry:%d", port) - suite.srv, _ = mockdns.NewServer(map[string]mockdns.Zone{ - "helm-test-registry.": { - A: []string{"127.0.0.1"}, - }, - }, false) - suite.srv.PatchNet(net.DefaultResolver) - - config.HTTP.Addr = fmt.Sprintf(":%d", port) - config.HTTP.DrainTimeout = time.Duration(10) * time.Second - config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} - - // Basic auth is not possible if we are serving HTTP. - if tlsEnabled { - config.Auth = configuration.Auth{ - "htpasswd": configuration.Parameters{ - "realm": "localhost", - "path": htpasswdPath, - }, - } - } - - // config tls - if tlsEnabled { - // TLS config - // this set tlsConf.ClientAuth = tls.RequireAndVerifyClientCert in the - // server tls config - config.HTTP.TLS.Certificate = tlsServerCert - config.HTTP.TLS.Key = tlsServerKey - // Skip client authentication if the registry is insecure. - if !insecure { - config.HTTP.TLS.ClientCAs = []string{tlsCA} - } - } - dockerRegistry, err := registry.NewRegistry(context.Background(), config) - suite.Nil(err, "no error creating test registry") - - suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() - return dockerRegistry -} - -func teardown(suite *TestSuite) { - if suite.srv != nil { - mockdns.UnpatchNet(net.DefaultResolver) - suite.srv.Close() - } -} - -func initCompromisedRegistryTestServer() string { - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if strings.Contains(r.URL.Path, "manifests") { - w.Header().Set("Content-Type", "application/vnd.oci.image.manifest.v1+json") - w.WriteHeader(200) - - // layers[0] is the blob []byte("a") - w.Write([]byte( - fmt.Sprintf(`{ "schemaVersion": 2, "config": { - "mediaType": "%s", - "digest": "sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133", - "size": 181 - }, - "layers": [ - { - "mediaType": "%s", - "digest": "sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb", - "size": 1 - } - ] -}`, ConfigMediaType, ChartLayerMediaType))) - } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:a705ee2789ab50a5ba20930f246dbd5cc01ff9712825bb98f57ee8414377f133" { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(200) - w.Write([]byte("{\"name\":\"mychart\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\\n" + - "an 'application' or a 'library' chart.\",\"apiVersion\":\"v2\",\"appVersion\":\"1.16.0\",\"type\":" + - "\"application\"}")) - } else if r.URL.Path == "/v2/testrepo/supposedlysafechart/blobs/sha256:ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb" { - w.Header().Set("Content-Type", ChartLayerMediaType) - w.WriteHeader(200) - w.Write([]byte("b")) - } else { - w.WriteHeader(500) - } - })) - - u, _ := url.Parse(s.URL) - return fmt.Sprintf("localhost:%s", u.Port()) -} - -func testPush(suite *TestSuite) { - // Bad bytes - ref := fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost) - _, err := suite.RegistryClient.Push([]byte("hello"), ref, PushOptTest(true)) - suite.NotNil(err, "error pushing non-chart bytes") - - // Load a test chart - chartData, err := os.ReadFile("../repo/repotest/testdata/examplechart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") - meta, err := extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") - - // non-strict ref (chart name) - ref = fmt.Sprintf("%s/testrepo/boop:%s", suite.DockerRegistryHost, meta.Version) - _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) - suite.NotNil(err, "error pushing non-strict ref (bad basename)") - - // non-strict ref (chart name), with strict mode disabled - _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true)) - suite.Nil(err, "no error pushing non-strict ref (bad basename), with strict mode disabled") - - // non-strict ref (chart version) - ref = fmt.Sprintf("%s/testrepo/%s:latest", suite.DockerRegistryHost, meta.Name) - _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) - suite.NotNil(err, "error pushing non-strict ref (bad tag)") - - // non-strict ref (chart version), with strict mode disabled - _, err = suite.RegistryClient.Push(chartData, ref, PushOptStrictMode(false), PushOptTest(true)) - suite.Nil(err, "no error pushing non-strict ref (bad tag), with strict mode disabled") - - // basic push, good ref - chartData, err = os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") - meta, err = extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") - ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) - _, err = suite.RegistryClient.Push(chartData, ref, PushOptTest(true)) - suite.Nil(err, "no error pushing good ref") - - _, err = suite.RegistryClient.Pull(ref) - suite.Nil(err, "no error pulling a simple chart") - - // Load another test chart - chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") - meta, err = extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") - - // Load prov file - provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") - suite.Nil(err, "no error loading test prov") - - // push with prov - ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) - result, err := suite.RegistryClient.Push(chartData, ref, PushOptProvData(provData), PushOptTest(true)) - suite.Nil(err, "no error pushing good ref with prov") - - _, err = suite.RegistryClient.Pull(ref) - suite.Nil(err, "no error pulling a simple chart") - - // Validate the output - // Note: these digests/sizes etc may change if the test chart/prov files are modified, - // or if the format of the OCI manifest changes - suite.Equal(ref, result.Ref) - suite.Equal(meta.Name, result.Chart.Meta.Name) - suite.Equal(meta.Version, result.Chart.Meta.Version) - suite.Equal(int64(684), result.Manifest.Size) - suite.Equal(int64(99), result.Config.Size) - suite.Equal(int64(973), result.Chart.Size) - suite.Equal(int64(695), result.Prov.Size) - suite.Equal( - "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", - result.Manifest.Digest) - suite.Equal( - "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", - result.Config.Digest) - suite.Equal( - "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", - result.Chart.Digest) - suite.Equal( - "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", - result.Prov.Digest) -} - -func testPull(suite *TestSuite) { - // bad/missing ref - ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost) - _, err := suite.RegistryClient.Pull(ref) - suite.NotNil(err, "error on bad/missing ref") - - // Load test chart (to build ref pushed in previous test) - chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") - meta, err := extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") - ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) - - // Simple pull, chart only - _, err = suite.RegistryClient.Pull(ref) - suite.Nil(err, "no error pulling a simple chart") - - // Simple pull with prov (no prov uploaded) - _, err = suite.RegistryClient.Pull(ref, PullOptWithProv(true)) - suite.NotNil(err, "error pulling a chart with prov when no prov exists") - - // Simple pull with prov, ignoring missing prov - _, err = suite.RegistryClient.Pull(ref, - PullOptWithProv(true), - PullOptIgnoreMissingProv(true)) - suite.Nil(err, - "no error pulling a chart with prov when no prov exists, ignoring missing") - - // Load test chart (to build ref pushed in previous test) - chartData, err = os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") - meta, err = extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") - ref = fmt.Sprintf("%s/testrepo/%s:%s", suite.DockerRegistryHost, meta.Name, meta.Version) - - // Load prov file - provData, err := os.ReadFile("../downloader/testdata/signtest-0.1.0.tgz.prov") - suite.Nil(err, "no error loading test prov") - - // no chart and no prov causes error - _, err = suite.RegistryClient.Pull(ref, - PullOptWithChart(false), - PullOptWithProv(false)) - suite.NotNil(err, "error on both no chart and no prov") - - // full pull with chart and prov - result, err := suite.RegistryClient.Pull(ref, PullOptWithProv(true)) - suite.Nil(err, "no error pulling a chart with prov") - - // Validate the output - // Note: these digests/sizes etc may change if the test chart/prov files are modified, - // or if the format of the OCI manifest changes - suite.Equal(ref, result.Ref) - suite.Equal(meta.Name, result.Chart.Meta.Name) - suite.Equal(meta.Version, result.Chart.Meta.Version) - suite.Equal(int64(684), result.Manifest.Size) - suite.Equal(int64(99), result.Config.Size) - suite.Equal(int64(973), result.Chart.Size) - suite.Equal(int64(695), result.Prov.Size) - suite.Equal( - "sha256:b57e8ffd938c43253f30afedb3c209136288e6b3af3b33473e95ea3b805888e6", - result.Manifest.Digest) - suite.Equal( - "sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580", - result.Config.Digest) - suite.Equal( - "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", - result.Chart.Digest) - suite.Equal( - "sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256", - result.Prov.Digest) - suite.Equal("{\"schemaVersion\":2,\"config\":{\"mediaType\":\"application/vnd.cncf.helm.config.v1+json\",\"digest\":\"sha256:8d17cb6bf6ccd8c29aace9a658495cbd5e2e87fc267876e86117c7db681c9580\",\"size\":99},\"layers\":[{\"mediaType\":\"application/vnd.cncf.helm.chart.provenance.v1.prov\",\"digest\":\"sha256:b0a02b7412f78ae93324d48df8fcc316d8482e5ad7827b5b238657a29a22f256\",\"size\":695},{\"mediaType\":\"application/vnd.cncf.helm.chart.content.v1.tar+gzip\",\"digest\":\"sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\",\"size\":973}],\"annotations\":{\"org.opencontainers.image.description\":\"A Helm chart for Kubernetes\",\"org.opencontainers.image.title\":\"signtest\",\"org.opencontainers.image.version\":\"0.1.0\"}}", - string(result.Manifest.Data)) - suite.Equal("{\"name\":\"signtest\",\"version\":\"0.1.0\",\"description\":\"A Helm chart for Kubernetes\",\"apiVersion\":\"v1\"}", - string(result.Config.Data)) - suite.Equal(chartData, result.Chart.Data) - suite.Equal(provData, result.Prov.Data) -} - -func testTags(suite *TestSuite) { - // Load test chart (to build ref pushed in previous test) - chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") - suite.Nil(err, "no error loading test chart") - meta, err := extractChartMeta(chartData) - suite.Nil(err, "no error extracting chart meta") - ref := fmt.Sprintf("%s/testrepo/%s", suite.DockerRegistryHost, meta.Name) - - // Query for tags and validate length - tags, err := suite.RegistryClient.Tags(ref) - suite.Nil(err, "no error retrieving tags") - suite.Equal(1, len(tags)) -} diff --git a/pkg/helm/pkg/release/common.go b/pkg/helm/pkg/release/common.go new file mode 100644 index 00000000..f5840c1d --- /dev/null +++ b/pkg/helm/pkg/release/common.go @@ -0,0 +1,116 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +import ( + "errors" + "fmt" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart" + v1release "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +var NewAccessor func(rel Releaser) (Accessor, error) = newDefaultAccessor //nolint:revive + +var NewHookAccessor func(rel Hook) (HookAccessor, error) = newDefaultHookAccessor //nolint:revive + +func newDefaultAccessor(rel Releaser) (Accessor, error) { + switch v := rel.(type) { + case v1release.Release: + return &v1Accessor{&v}, nil + case *v1release.Release: + return &v1Accessor{v}, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} + +func newDefaultHookAccessor(hook Hook) (HookAccessor, error) { + switch h := hook.(type) { + case v1release.Hook: + return &v1HookAccessor{&h}, nil + case *v1release.Hook: + return &v1HookAccessor{h}, nil + default: + return nil, errors.New("unsupported release hook type") + } +} + +type v1Accessor struct { + rel *v1release.Release +} + +func (a *v1Accessor) Name() string { + return a.rel.Name +} + +func (a *v1Accessor) Namespace() string { + return a.rel.Namespace +} + +func (a *v1Accessor) Version() int { + return a.rel.Version +} + +func (a *v1Accessor) Hooks() []Hook { + var hooks = make([]Hook, len(a.rel.Hooks)) + for i, h := range a.rel.Hooks { + hooks[i] = h + } + return hooks +} + +func (a *v1Accessor) Manifest() string { + return a.rel.Manifest +} + +func (a *v1Accessor) Notes() string { + return a.rel.Info.Notes +} + +func (a *v1Accessor) Labels() map[string]string { + return a.rel.Labels +} + +func (a *v1Accessor) Chart() chart.Charter { + return a.rel.Chart +} + +func (a *v1Accessor) Status() string { + return a.rel.Info.Status.String() +} + +func (a *v1Accessor) ApplyMethod() string { + return a.rel.ApplyMethod +} + +func (a *v1Accessor) DeployedAt() time.Time { + return a.rel.Info.LastDeployed +} + +type v1HookAccessor struct { + hook *v1release.Hook +} + +func (a *v1HookAccessor) Path() string { + return a.hook.Path +} + +func (a *v1HookAccessor) Manifest() string { + return a.hook.Manifest +} diff --git a/pkg/helm/pkg/release/common/status.go b/pkg/helm/pkg/release/common/status.go new file mode 100644 index 00000000..fd501030 --- /dev/null +++ b/pkg/helm/pkg/release/common/status.go @@ -0,0 +1,49 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +// Status is the status of a release +type Status string + +// Describe the status of a release +// NOTE: Make sure to update cmd/helm/status.go when adding or modifying any of these statuses. +const ( + // StatusUnknown indicates that a release is in an uncertain state. + StatusUnknown Status = "unknown" + // StatusDeployed indicates that the release has been pushed to Kubernetes. + StatusDeployed Status = "deployed" + // StatusUninstalled indicates that a release has been uninstalled from Kubernetes. + StatusUninstalled Status = "uninstalled" + // StatusSuperseded indicates that this release object is outdated and a newer one exists. + StatusSuperseded Status = "superseded" + // StatusFailed indicates that the release was not successfully deployed. + StatusFailed Status = "failed" + // StatusUninstalling indicates that an uninstall operation is underway. + StatusUninstalling Status = "uninstalling" + // StatusPendingInstall indicates that an install operation is underway. + StatusPendingInstall Status = "pending-install" + // StatusPendingUpgrade indicates that an upgrade operation is underway. + StatusPendingUpgrade Status = "pending-upgrade" + // StatusPendingRollback indicates that a rollback operation is underway. + StatusPendingRollback Status = "pending-rollback" +) + +func (x Status) String() string { return string(x) } + +// IsPending determines if this status is a state or a transition. +func (x Status) IsPending() bool { + return x == StatusPendingInstall || x == StatusPendingUpgrade || x == StatusPendingRollback +} diff --git a/pkg/helm/pkg/release/common_test.go b/pkg/helm/pkg/release/common_test.go new file mode 100644 index 00000000..cdfbadb7 --- /dev/null +++ b/pkg/helm/pkg/release/common_test.go @@ -0,0 +1,65 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +func TestNewDefaultAccessor(t *testing.T) { + // Testing the default implementation rather than NewAccessor which can be + // overridden by developers. + is := assert.New(t) + + // Create release + info := &rspb.Info{Status: common.StatusDeployed, LastDeployed: time.Now().Add(1000)} + labels := make(map[string]string) + labels["foo"] = "bar" + rel := &rspb.Release{ + Name: "happy-cats", + Version: 2, + Info: info, + Labels: labels, + Namespace: "default", + ApplyMethod: "csa", + } + + // newDefaultAccessor should not be called directly Instead, NewAccessor should be + // called and it will call NewDefaultAccessor. NewAccessor can be changed to a + // non-default accessor by a user so the test calls the default implementation. + // The accessor provides a means to access data on resources that are different types + // but have the same interface. Instead of properties, methods are used to access + // information. Structs with properties are useful in Go when it comes to marshalling + // and unmarshalling data (e.g. coming and going from JSON or YAML). But, structs + // can't be used with interfaces. The accessors enable access to the underlying data + // in a manner that works with Go interfaces. + accessor, err := newDefaultAccessor(rel) + is.NoError(err) + + // Verify information + is.Equal(rel.Name, accessor.Name()) + is.Equal(rel.Namespace, accessor.Namespace()) + is.Equal(rel.Version, accessor.Version()) + is.Equal(rel.ApplyMethod, accessor.ApplyMethod()) + is.Equal(rel.Labels, accessor.Labels()) +} diff --git a/pkg/helm/pkg/release/hook.go b/pkg/helm/pkg/release/hook.go deleted file mode 100644 index 4a6a725e..00000000 --- a/pkg/helm/pkg/release/hook.go +++ /dev/null @@ -1,113 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package release - -import ( - "github.com/werf/nelm/pkg/helm/pkg/time" -) - -// HookEvent specifies the hook event -type HookEvent string - -// Hook event types -const ( - HookPreInstall HookEvent = "pre-install" - HookPostInstall HookEvent = "post-install" - HookPreDelete HookEvent = "pre-delete" - HookPostDelete HookEvent = "post-delete" - HookPreUpgrade HookEvent = "pre-upgrade" - HookPostUpgrade HookEvent = "post-upgrade" - HookPreRollback HookEvent = "pre-rollback" - HookPostRollback HookEvent = "post-rollback" - HookTest HookEvent = "test" -) - -func (x HookEvent) String() string { return string(x) } - -// HookDeletePolicy specifies the hook delete policy -type HookDeletePolicy string - -// Hook delete policy types -const ( - HookSucceeded HookDeletePolicy = "hook-succeeded" - HookFailed HookDeletePolicy = "hook-failed" - HookBeforeHookCreation HookDeletePolicy = "before-hook-creation" -) - -func (x HookDeletePolicy) String() string { return string(x) } - -// HookAnnotation is the label name for a hook -const HookAnnotation = "helm.sh/hook" - -// HookWeightAnnotation is the label name for a hook weight -const HookWeightAnnotation = "helm.sh/hook-weight" - -// HookDeleteAnnotation is the label name for the delete policy for a hook -const HookDeleteAnnotation = "helm.sh/hook-delete-policy" - -// Hook defines a hook object. -type Hook struct { - Name string `json:"name,omitempty"` - // Kind is the Kubernetes kind. - Kind string `json:"kind,omitempty"` - // Path is the chart-relative path to the template. - Path string `json:"path,omitempty"` - // Manifest is the manifest contents. - Manifest string `json:"manifest,omitempty"` - // Events are the events that this hook fires on. - Events []HookEvent `json:"events,omitempty"` - // LastRun indicates the date/time this was last run. - LastRun HookExecution `json:"last_run,omitempty"` - // Weight indicates the sort order for execution among similar Hook type - Weight int `json:"weight,omitempty"` - // DeletePolicies are the policies that indicate when to delete the hook - DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"` -} - -// A HookExecution records the result for the last execution of a hook for a given release. -type HookExecution struct { - // StartedAt indicates the date/time this hook was started - StartedAt time.Time `json:"started_at,omitempty"` - // CompletedAt indicates the date/time this hook was completed. - CompletedAt time.Time `json:"completed_at,omitempty"` - // Phase indicates whether the hook completed successfully - Phase HookPhase `json:"phase"` -} - -// A HookPhase indicates the state of a hook execution -type HookPhase string - -const ( - // HookPhaseUnknown indicates that a hook is in an unknown state - HookPhaseUnknown HookPhase = "Unknown" - // HookPhaseRunning indicates that a hook is currently executing - HookPhaseRunning HookPhase = "Running" - // HookPhaseSucceeded indicates that hook execution succeeded - HookPhaseSucceeded HookPhase = "Succeeded" - // HookPhaseFailed indicates that hook execution failed - HookPhaseFailed HookPhase = "Failed" -) - -// String converts a hook phase to a printable string -func (x HookPhase) String() string { return string(x) } - -const ( - HookInstall HookEvent = "install" - HookUpgrade HookEvent = "upgrade" - HookRollback HookEvent = "rollback" - HookDelete HookEvent = "delete" -) diff --git a/pkg/helm/pkg/release/info.go b/pkg/helm/pkg/release/info.go deleted file mode 100644 index ec5400ae..00000000 --- a/pkg/helm/pkg/release/info.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package release - -import ( - "k8s.io/apimachinery/pkg/runtime" - - "github.com/werf/nelm/pkg/helm/pkg/time" -) - -// Info describes release information. -type Info struct { - // FirstDeployed is when the release was first deployed. - FirstDeployed time.Time `json:"first_deployed,omitempty"` - // LastDeployed is when the release was last deployed. - LastDeployed time.Time `json:"last_deployed,omitempty"` - // Deleted tracks when this object was deleted. - Deleted time.Time `json:"deleted"` - // Description is human-friendly "log entry" about this release. - Description string `json:"description,omitempty"` - // Status is the current state of the release - Status Status `json:"status,omitempty"` - // Contains the rendered templates/NOTES.txt if available - Notes string `json:"notes,omitempty"` - // Contains the deployed resources information - Resources map[string][]runtime.Object `json:"resources,omitempty"` - - LastPhase *Phase `json:"last_phase,omitempty"` - LastStage *int `json:"last_stage,omitempty"` - Annotations map[string]string `json:"annotations,omitempty"` -} diff --git a/pkg/helm/pkg/release/interfaces.go b/pkg/helm/pkg/release/interfaces.go new file mode 100644 index 00000000..abadf899 --- /dev/null +++ b/pkg/helm/pkg/release/interfaces.go @@ -0,0 +1,46 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +import ( + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart" +) + +type Releaser interface{} + +type Hook interface{} + +type Accessor interface { + Name() string + Namespace() string + Version() int + Hooks() []Hook + Manifest() string + Notes() string + Labels() map[string]string + Chart() chart.Charter + Status() string + ApplyMethod() string + DeployedAt() time.Time +} + +type HookAccessor interface { + Path() string + Manifest() string +} diff --git a/pkg/helm/pkg/release/mock.go b/pkg/helm/pkg/release/mock.go deleted file mode 100644 index 7f329be9..00000000 --- a/pkg/helm/pkg/release/mock.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package release - -import ( - "fmt" - "math/rand" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/time" -) - -// MockHookTemplate is the hook template used for all mock release objects. -var MockHookTemplate = `apiVersion: v1 -kind: Job -metadata: - annotations: - "helm.sh/hook": pre-install -` - -// MockManifest is the manifest used for all mock release objects. -var MockManifest = `apiVersion: v1 -kind: Secret -metadata: - name: fixture -` - -// MockReleaseOptions allows for user-configurable options on mock release objects. -type MockReleaseOptions struct { - Name string - Version int - Chart *chart.Chart - Status Status - Namespace string -} - -// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. -func Mock(opts *MockReleaseOptions) *Release { - date := time.Unix(242085845, 0).UTC() - - name := opts.Name - if name == "" { - name = "testrelease-" + fmt.Sprint(rand.Intn(100)) - } - - version := 1 - if opts.Version != 0 { - version = opts.Version - } - - namespace := opts.Namespace - if namespace == "" { - namespace = "default" - } - - ch := opts.Chart - if opts.Chart == nil { - ch = &chart.Chart{ - Metadata: &chart.Metadata{ - Name: "foo", - Version: "0.1.0-beta.1", - AppVersion: "1.0", - }, - Templates: []*chart.File{ - {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, - }, - } - } - - scode := StatusDeployed - if len(opts.Status) > 0 { - scode = opts.Status - } - - info := &Info{ - FirstDeployed: date, - LastDeployed: date, - Status: scode, - Description: "Release mock", - Notes: "Some mock release notes!", - } - - return &Release{ - Name: name, - Info: info, - Chart: ch, - Config: map[string]interface{}{"name": "value"}, - Version: version, - Namespace: namespace, - Hooks: []*Hook{ - { - Name: "pre-install-hook", - Kind: "Job", - Path: "pre-install-hook.yaml", - Manifest: MockHookTemplate, - LastRun: HookExecution{}, - Events: []HookEvent{HookPreInstall}, - }, - }, - Manifest: MockManifest, - } -} diff --git a/pkg/helm/pkg/release/phase.go b/pkg/helm/pkg/release/phase.go deleted file mode 100644 index 86b66439..00000000 --- a/pkg/helm/pkg/release/phase.go +++ /dev/null @@ -1,62 +0,0 @@ -package release - -import "fmt" - -type Phase string - -const ( - PhaseInit Phase = "init" - PhaseHooksPre Phase = "hooks-pre" - PhaseRollout Phase = "rollout" - PhaseUninstall Phase = "uninstall" - PhaseHooksPost Phase = "hooks-post" -) - -// May return empty string. -func PhaseFromHookEvent(hookEvent HookEvent) Phase { - var phase Phase - switch hookEvent { - case HookPreInstall, HookPreDelete, HookPreUpgrade, HookPreRollback: - phase = PhaseHooksPre - case HookPostInstall, HookPostDelete, HookPostUpgrade, HookPostRollback: - phase = PhaseHooksPost - case HookTest: - default: - panic(fmt.Sprintf("unexpected HookEvent: %s", hookEvent.String())) - } - - return phase -} - -func SetInitPhaseStageInfo(rel *Release) *Release { - lastPhase := PhaseInit - lastStage := 0 - rel.Info.LastPhase = &lastPhase - rel.Info.LastStage = &lastStage - - return rel -} - -func SetHookPhaseStageInfo(rel *Release, hookIndex int, hook HookEvent) *Release { - lastPhase := PhaseFromHookEvent(hook) - rel.Info.LastPhase = &lastPhase - rel.Info.LastStage = &hookIndex - - return rel -} - -func SetRolloutPhaseStageInfo(rel *Release, stageIndex int) *Release { - lastPhase := PhaseRollout - rel.Info.LastPhase = &lastPhase - rel.Info.LastStage = &stageIndex - - return rel -} - -func SetUninstallPhaseStageInfo(rel *Release) *Release { - lastPhase := PhaseUninstall - rel.Info.LastPhase = &lastPhase - rel.Info.LastStage = nil - - return rel -} diff --git a/pkg/helm/pkg/release/release.go b/pkg/helm/pkg/release/release.go deleted file mode 100644 index 07bf45fa..00000000 --- a/pkg/helm/pkg/release/release.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package release - -import ( - "fmt" - - "github.com/werf/nelm/pkg/helm/pkg/chart" -) - -// Release describes a deployment of a chart, together with the chart -// and the variables used to deploy that chart. -type Release struct { - // Name is the name of the release - Name string `json:"name,omitempty"` - // Info provides information about a release - Info *Info `json:"info,omitempty"` - // Chart is the chart that was released. - Chart *chart.Chart `json:"chart,omitempty"` - // Config is the set of extra Values added to the chart. - // These values override the default values inside of the chart. - Config map[string]interface{} `json:"config,omitempty"` - // Manifest is the string representation of the rendered template. - Manifest string `json:"manifest,omitempty"` - // Hooks are all of the hooks declared for this release. - Hooks []*Hook `json:"hooks,omitempty"` - // Version is an int which represents the revision of the release. - Version int `json:"version,omitempty"` - // Namespace is the kubernetes namespace of the release. - Namespace string `json:"namespace,omitempty"` - // Labels of the release. - // Disabled encoding into Json cause labels are stored in storage driver metadata field. - Labels map[string]string `json:"-"` - - UnstoredManifest string `json:"-"` -} - -// SetStatus is a helper for setting the status on a release. -func (r *Release) SetStatus(status Status, msg string) { - r.Info.Status = status - r.Info.Description = msg -} - -func (r *Release) IsStatusSucceeded() bool { - switch r.Info.Status { - case StatusDeployed, - StatusSuperseded, - StatusUninstalled: - return true - default: - return false - } -} - -func (r *Release) IsStatusFailed() bool { - switch r.Info.Status { - case StatusFailed, - StatusUnknown, - StatusPendingInstall, - StatusPendingUpgrade, - StatusPendingRollback, - StatusUninstalling: - return true - default: - return false - } -} - -func (r *Release) ID() string { - return ReleaseID(r.Namespace, r.Name, r.Version) -} - -func (r *Release) IDHuman() string { - return ReleaseIDHuman(r.Namespace, r.Name, r.Version) -} - -func ReleaseID(namespace, name string, revision int) string { - return fmt.Sprintf("%s:%s:%d", namespace, name, revision) -} - -func ReleaseIDHuman(namespace, name string, revision int) string { - return fmt.Sprintf("%s/%d (namespace=%s)", name, revision, namespace) -} diff --git a/pkg/helm/pkg/release/report.go b/pkg/helm/pkg/release/report.go deleted file mode 100644 index 7ee8bc3b..00000000 --- a/pkg/helm/pkg/release/report.go +++ /dev/null @@ -1,45 +0,0 @@ -package release - -import ( - "encoding/json" - "fmt" - - "github.com/werf/nelm/pkg/helm/pkg/time" -) - -func NewDeployReport() *DeployReport { - return &DeployReport{} -} - -type DeployReport struct { - Release string `json:"release,omitempty"` - Namespace string `json:"namespace,omitempty"` - Revision int `json:"revision,omitempty"` - Status Status `json:"status,omitempty"` - LastPhase *Phase `json:"last_phase,omitempty"` - LastStage *int `json:"last_stage,omitempty"` - FirstDeployedTime time.Time `json:"first_deployed,omitempty"` - LastDeployedTime time.Time `json:"last_deployed,omitempty"` -} - -func (r *DeployReport) FromRelease(release *Release) *DeployReport { - r.Release = release.Name - r.Namespace = release.Namespace - r.Revision = release.Version - r.Status = release.Info.Status - r.LastPhase = release.Info.LastPhase - r.LastStage = release.Info.LastStage - r.FirstDeployedTime = release.Info.FirstDeployed - r.LastDeployedTime = release.Info.LastDeployed - - return r -} - -func (r *DeployReport) ToJSONData() ([]byte, error) { - data, err := json.MarshalIndent(r, "", "\t") - if err != nil { - return nil, fmt.Errorf("error marshalling deploy report: %w", err) - } - - return data, nil -} diff --git a/pkg/helm/pkg/release/responses.go b/pkg/helm/pkg/release/responses.go index 7ee1fc2e..6e0a0eae 100644 --- a/pkg/helm/pkg/release/responses.go +++ b/pkg/helm/pkg/release/responses.go @@ -18,7 +18,7 @@ package release // UninstallReleaseResponse represents a successful response to an uninstall request. type UninstallReleaseResponse struct { // Release is the release that was marked deleted. - Release *Release `json:"release,omitempty"` + Release Releaser `json:"release,omitempty"` // Info is an uninstall message Info string `json:"info,omitempty"` } diff --git a/pkg/helm/pkg/release/status.go b/pkg/helm/pkg/release/status.go deleted file mode 100644 index 8b4c2068..00000000 --- a/pkg/helm/pkg/release/status.go +++ /dev/null @@ -1,53 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package release - -// Status is the status of a release -type Status string - -// Describe the status of a release -// NOTE: Make sure to update cmd/helm/status.go when adding or modifying any of these statuses. -const ( - // StatusUnknown indicates that a release is in an uncertain state. - StatusUnknown Status = "unknown" - // StatusDeployed indicates that the release has been pushed to Kubernetes. - StatusDeployed Status = "deployed" - // StatusUninstalled indicates that a release has been uninstalled from Kubernetes. - StatusUninstalled Status = "uninstalled" - // StatusSuperseded indicates that this release object is outdated and a newer one exists. - StatusSuperseded Status = "superseded" - // StatusFailed indicates that the release was not successfully deployed. - StatusFailed Status = "failed" - // StatusUninstalling indicates that a uninstall operation is underway. - StatusUninstalling Status = "uninstalling" - // StatusPendingInstall indicates that an install operation is underway. - StatusPendingInstall Status = "pending-install" - // StatusPendingUpgrade indicates that an upgrade operation is underway. - StatusPendingUpgrade Status = "pending-upgrade" - // StatusPendingRollback indicates that an rollback operation is underway. - StatusPendingRollback Status = "pending-rollback" -) - -func (x Status) String() string { return string(x) } - -// IsPending determines if this status is a state or a transition. -func (x Status) IsPending() bool { - return x == StatusPendingInstall || x == StatusPendingUpgrade || x == StatusPendingRollback -} - -const ( - StatusSkipped Status = "skipped" -) diff --git a/pkg/helm/pkg/release/v1/hook.go b/pkg/helm/pkg/release/v1/hook.go new file mode 100644 index 00000000..f0a370c1 --- /dev/null +++ b/pkg/helm/pkg/release/v1/hook.go @@ -0,0 +1,189 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "time" +) + +// HookEvent specifies the hook event +type HookEvent string + +// Hook event types +const ( + HookPreInstall HookEvent = "pre-install" + HookPostInstall HookEvent = "post-install" + HookPreDelete HookEvent = "pre-delete" + HookPostDelete HookEvent = "post-delete" + HookPreUpgrade HookEvent = "pre-upgrade" + HookPostUpgrade HookEvent = "post-upgrade" + HookPreRollback HookEvent = "pre-rollback" + HookPostRollback HookEvent = "post-rollback" + HookTest HookEvent = "test" +) + +func (x HookEvent) String() string { return string(x) } + +// HookDeletePolicy specifies the hook delete policy +type HookDeletePolicy string + +// Hook delete policy types +const ( + HookSucceeded HookDeletePolicy = "hook-succeeded" + HookFailed HookDeletePolicy = "hook-failed" + HookBeforeHookCreation HookDeletePolicy = "before-hook-creation" +) + +func (x HookDeletePolicy) String() string { return string(x) } + +// HookOutputLogPolicy specifies the hook output log policy +type HookOutputLogPolicy string + +// Hook output log policy types +const ( + HookOutputOnSucceeded HookOutputLogPolicy = "hook-succeeded" + HookOutputOnFailed HookOutputLogPolicy = "hook-failed" +) + +func (x HookOutputLogPolicy) String() string { return string(x) } + +// HookAnnotation is the label name for a hook +const HookAnnotation = "helm.sh/hook" + +// HookWeightAnnotation is the label name for a hook weight +const HookWeightAnnotation = "helm.sh/hook-weight" + +// HookDeleteAnnotation is the label name for the delete policy for a hook +const HookDeleteAnnotation = "helm.sh/hook-delete-policy" + +// HookOutputLogAnnotation is the label name for the output log policy for a hook +const HookOutputLogAnnotation = "helm.sh/hook-output-log-policy" + +// Hook defines a hook object. +type Hook struct { + Name string `json:"name,omitempty"` + // Kind is the Kubernetes kind. + Kind string `json:"kind,omitempty"` + // Path is the chart-relative path to the template. + Path string `json:"path,omitempty"` + // Manifest is the manifest contents. + Manifest string `json:"manifest,omitempty"` + // Events are the events that this hook fires on. + Events []HookEvent `json:"events,omitempty"` + // LastRun indicates the date/time this was last run. + LastRun HookExecution `json:"last_run,omitempty"` + // Weight indicates the sort order for execution among similar Hook type + Weight int `json:"weight,omitempty"` + // DeletePolicies are the policies that indicate when to delete the hook + DeletePolicies []HookDeletePolicy `json:"delete_policies,omitempty"` + // OutputLogPolicies defines whether we should copy hook logs back to main process + OutputLogPolicies []HookOutputLogPolicy `json:"output_log_policies,omitempty"` +} + +// A HookExecution records the result for the last execution of a hook for a given release. +type HookExecution struct { + // StartedAt indicates the date/time this hook was started + StartedAt time.Time `json:"started_at,omitzero"` + // CompletedAt indicates the date/time this hook was completed. + CompletedAt time.Time `json:"completed_at,omitzero"` + // Phase indicates whether the hook completed successfully + Phase HookPhase `json:"phase"` +} + +// A HookPhase indicates the state of a hook execution +type HookPhase string + +const ( + // HookPhaseUnknown indicates that a hook is in an unknown state + HookPhaseUnknown HookPhase = "Unknown" + // HookPhaseRunning indicates that a hook is currently executing + HookPhaseRunning HookPhase = "Running" + // HookPhaseSucceeded indicates that hook execution succeeded + HookPhaseSucceeded HookPhase = "Succeeded" + // HookPhaseFailed indicates that hook execution failed + HookPhaseFailed HookPhase = "Failed" +) + +// String converts a hook phase to a printable string +func (x HookPhase) String() string { return string(x) } + +// hookExecutionJSON is used for custom JSON marshaling/unmarshaling +type hookExecutionJSON struct { + StartedAt *time.Time `json:"started_at,omitempty"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + Phase HookPhase `json:"phase"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It handles empty string time fields by treating them as zero values. +func (h *HookExecution) UnmarshalJSON(data []byte) error { + // First try to unmarshal into a map to handle empty string time fields + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Replace empty string time fields with nil + for _, field := range []string{"started_at", "completed_at"} { + if val, ok := raw[field]; ok { + if str, ok := val.(string); ok && str == "" { + raw[field] = nil + } + } + } + + // Re-marshal with cleaned data + cleaned, err := json.Marshal(raw) + if err != nil { + return err + } + + // Unmarshal into temporary struct with pointer time fields + var tmp hookExecutionJSON + if err := json.Unmarshal(cleaned, &tmp); err != nil { + return err + } + + // Copy values to HookExecution struct + if tmp.StartedAt != nil { + h.StartedAt = *tmp.StartedAt + } + if tmp.CompletedAt != nil { + h.CompletedAt = *tmp.CompletedAt + } + h.Phase = tmp.Phase + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +// It omits zero-value time fields from the JSON output. +func (h HookExecution) MarshalJSON() ([]byte, error) { + tmp := hookExecutionJSON{ + Phase: h.Phase, + } + + if !h.StartedAt.IsZero() { + tmp.StartedAt = &h.StartedAt + } + if !h.CompletedAt.IsZero() { + tmp.CompletedAt = &h.CompletedAt + } + + return json.Marshal(tmp) +} diff --git a/pkg/helm/pkg/release/v1/hook_test.go b/pkg/helm/pkg/release/v1/hook_test.go new file mode 100644 index 00000000..cea2568b --- /dev/null +++ b/pkg/helm/pkg/release/v1/hook_test.go @@ -0,0 +1,231 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHookExecutionMarshalJSON(t *testing.T) { + started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC) + + tests := []struct { + name string + exec HookExecution + expected string + }{ + { + name: "all fields populated", + exec: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseSucceeded, + }, + expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`, + }, + { + name: "only phase", + exec: HookExecution{ + Phase: HookPhaseRunning, + }, + expected: `{"phase":"Running"}`, + }, + { + name: "with started time only", + exec: HookExecution{ + StartedAt: started, + Phase: HookPhaseRunning, + }, + expected: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`, + }, + { + name: "failed phase", + exec: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseFailed, + }, + expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`, + }, + { + name: "unknown phase", + exec: HookExecution{ + Phase: HookPhaseUnknown, + }, + expected: `{"phase":"Unknown"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.exec) + require.NoError(t, err) + assert.JSONEq(t, tt.expected, string(data)) + }) + } +} + +func TestHookExecutionUnmarshalJSON(t *testing.T) { + started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC) + + tests := []struct { + name string + input string + expected HookExecution + wantErr bool + }{ + { + name: "all fields populated", + input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`, + expected: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseSucceeded, + }, + }, + { + name: "only phase", + input: `{"phase":"Running"}`, + expected: HookExecution{ + Phase: HookPhaseRunning, + }, + }, + { + name: "empty string time fields", + input: `{"started_at":"","completed_at":"","phase":"Succeeded"}`, + expected: HookExecution{ + Phase: HookPhaseSucceeded, + }, + }, + { + name: "missing time fields", + input: `{"phase":"Failed"}`, + expected: HookExecution{ + Phase: HookPhaseFailed, + }, + }, + { + name: "null time fields", + input: `{"started_at":null,"completed_at":null,"phase":"Unknown"}`, + expected: HookExecution{ + Phase: HookPhaseUnknown, + }, + }, + { + name: "mixed empty and valid time fields", + input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"","phase":"Running"}`, + expected: HookExecution{ + StartedAt: started, + Phase: HookPhaseRunning, + }, + }, + { + name: "with started time only", + input: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`, + expected: HookExecution{ + StartedAt: started, + Phase: HookPhaseRunning, + }, + }, + { + name: "failed phase with times", + input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`, + expected: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseFailed, + }, + }, + { + name: "invalid time format", + input: `{"started_at":"invalid-time","phase":"Running"}`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var exec HookExecution + err := json.Unmarshal([]byte(tt.input), &exec) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.StartedAt.Unix(), exec.StartedAt.Unix()) + assert.Equal(t, tt.expected.CompletedAt.Unix(), exec.CompletedAt.Unix()) + assert.Equal(t, tt.expected.Phase, exec.Phase) + }) + } +} + +func TestHookExecutionRoundTrip(t *testing.T) { + started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC) + + original := HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseSucceeded, + } + + data, err := json.Marshal(&original) + require.NoError(t, err) + + var decoded HookExecution + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.StartedAt.Unix(), decoded.StartedAt.Unix()) + assert.Equal(t, original.CompletedAt.Unix(), decoded.CompletedAt.Unix()) + assert.Equal(t, original.Phase, decoded.Phase) +} + +func TestHookExecutionEmptyStringRoundTrip(t *testing.T) { + // This test specifically verifies that empty string time fields + // are handled correctly during parsing + input := `{"started_at":"","completed_at":"","phase":"Succeeded"}` + + var exec HookExecution + err := json.Unmarshal([]byte(input), &exec) + require.NoError(t, err) + + // Verify time fields are zero values + assert.True(t, exec.StartedAt.IsZero()) + assert.True(t, exec.CompletedAt.IsZero()) + assert.Equal(t, HookPhaseSucceeded, exec.Phase) + + // Marshal back and verify empty time fields are omitted + data, err := json.Marshal(&exec) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Zero time values should be omitted + assert.NotContains(t, result, "started_at") + assert.NotContains(t, result, "completed_at") + assert.Equal(t, "Succeeded", result["phase"]) +} diff --git a/pkg/helm/pkg/release/v1/info.go b/pkg/helm/pkg/release/v1/info.go new file mode 100644 index 00000000..d1aaacbd --- /dev/null +++ b/pkg/helm/pkg/release/v1/info.go @@ -0,0 +1,126 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" + + "k8s.io/apimachinery/pkg/runtime" +) + +// Info describes release information. +type Info struct { + // FirstDeployed is when the release was first deployed. + FirstDeployed time.Time `json:"first_deployed,omitzero"` + // LastDeployed is when the release was last deployed. + LastDeployed time.Time `json:"last_deployed,omitzero"` + // Deleted tracks when this object was deleted. + Deleted time.Time `json:"deleted,omitzero"` + // Description is human-friendly "log entry" about this release. + Description string `json:"description,omitempty"` + // Status is the current state of the release + Status common.Status `json:"status,omitempty"` + // Contains the rendered templates/NOTES.txt if available + Notes string `json:"notes,omitempty"` + // Contains the deployed resources information + Resources map[string][]runtime.Object `json:"resources,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +// infoJSON is used for custom JSON marshaling/unmarshaling +type infoJSON struct { + FirstDeployed *time.Time `json:"first_deployed,omitempty"` + LastDeployed *time.Time `json:"last_deployed,omitempty"` + Deleted *time.Time `json:"deleted,omitempty"` + Description string `json:"description,omitempty"` + Status common.Status `json:"status,omitempty"` + Notes string `json:"notes,omitempty"` + Resources map[string][]runtime.Object `json:"resources,omitempty"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It handles empty string time fields by treating them as zero values. +func (i *Info) UnmarshalJSON(data []byte) error { + // First try to unmarshal into a map to handle empty string time fields + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Replace empty string time fields with nil + for _, field := range []string{"first_deployed", "last_deployed", "deleted"} { + if val, ok := raw[field]; ok { + if str, ok := val.(string); ok && str == "" { + raw[field] = nil + } + } + } + + // Re-marshal with cleaned data + cleaned, err := json.Marshal(raw) + if err != nil { + return err + } + + // Unmarshal into temporary struct with pointer time fields + var tmp infoJSON + if err := json.Unmarshal(cleaned, &tmp); err != nil { + return err + } + + // Copy values to Info struct + if tmp.FirstDeployed != nil { + i.FirstDeployed = *tmp.FirstDeployed + } + if tmp.LastDeployed != nil { + i.LastDeployed = *tmp.LastDeployed + } + if tmp.Deleted != nil { + i.Deleted = *tmp.Deleted + } + i.Description = tmp.Description + i.Status = tmp.Status + i.Notes = tmp.Notes + i.Resources = tmp.Resources + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +// It omits zero-value time fields from the JSON output. +func (i Info) MarshalJSON() ([]byte, error) { + tmp := infoJSON{ + Description: i.Description, + Status: i.Status, + Notes: i.Notes, + Resources: i.Resources, + } + + if !i.FirstDeployed.IsZero() { + tmp.FirstDeployed = &i.FirstDeployed + } + if !i.LastDeployed.IsZero() { + tmp.LastDeployed = &i.LastDeployed + } + if !i.Deleted.IsZero() { + tmp.Deleted = &i.Deleted + } + + return json.Marshal(tmp) +} diff --git a/pkg/helm/pkg/release/v1/info_test.go b/pkg/helm/pkg/release/v1/info_test.go new file mode 100644 index 00000000..4c3cca17 --- /dev/null +++ b/pkg/helm/pkg/release/v1/info_test.go @@ -0,0 +1,285 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInfoMarshalJSON(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC) + + tests := []struct { + name string + info Info + expected string + }{ + { + name: "all fields populated", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Description: "Test release", + Status: common.StatusDeployed, + Notes: "Test notes", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`, + }, + { + name: "only required fields", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`, + }, + { + name: "zero time values omitted", + info: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + expected: `{"description":"Test release","status":"deployed"}`, + }, + { + name: "with pending status", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusPendingInstall, + Description: "Installing release", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","description":"Installing release","status":"pending-install"}`, + }, + { + name: "uninstalled with deleted time", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Status: common.StatusUninstalled, + Description: "Uninstalled release", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.info) + require.NoError(t, err) + assert.JSONEq(t, tt.expected, string(data)) + }) + } +} + +func TestInfoUnmarshalJSON(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC) + + tests := []struct { + name string + input string + expected Info + wantErr bool + }{ + { + name: "all fields populated", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Description: "Test release", + Status: common.StatusDeployed, + Notes: "Test notes", + }, + }, + { + name: "only required fields", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + }, + }, + { + name: "empty string time fields", + input: `{"first_deployed":"","last_deployed":"","deleted":"","description":"Test release","status":"deployed"}`, + expected: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + }, + { + name: "missing time fields", + input: `{"description":"Test release","status":"deployed"}`, + expected: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + }, + { + name: "null time fields", + input: `{"first_deployed":null,"last_deployed":null,"deleted":null,"description":"Test release","status":"deployed"}`, + expected: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + }, + { + name: "mixed empty and valid time fields", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"","deleted":"","status":"deployed"}`, + expected: Info{ + FirstDeployed: now, + Status: common.StatusDeployed, + }, + }, + { + name: "pending install status", + input: `{"first_deployed":"2025-10-08T12:00:00Z","status":"pending-install","description":"Installing"}`, + expected: Info{ + FirstDeployed: now, + Status: common.StatusPendingInstall, + Description: "Installing", + }, + }, + { + name: "uninstalled with deleted time", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","status":"uninstalled"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Status: common.StatusUninstalled, + }, + }, + { + name: "failed status", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"failed","description":"Deployment failed"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusFailed, + Description: "Deployment failed", + }, + }, + { + name: "invalid time format", + input: `{"first_deployed":"invalid-time","status":"deployed"}`, + wantErr: true, + }, + { + name: "empty object", + input: `{}`, + expected: Info{ + Status: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var info Info + err := json.Unmarshal([]byte(tt.input), &info) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.FirstDeployed.Unix(), info.FirstDeployed.Unix()) + assert.Equal(t, tt.expected.LastDeployed.Unix(), info.LastDeployed.Unix()) + assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix()) + assert.Equal(t, tt.expected.Description, info.Description) + assert.Equal(t, tt.expected.Status, info.Status) + assert.Equal(t, tt.expected.Notes, info.Notes) + assert.Equal(t, tt.expected.Resources, info.Resources) + }) + } +} + +func TestInfoRoundTrip(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + + original := Info{ + FirstDeployed: now, + LastDeployed: later, + Description: "Test release", + Status: common.StatusDeployed, + Notes: "Release notes", + } + + data, err := json.Marshal(&original) + require.NoError(t, err) + + var decoded Info + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.FirstDeployed.Unix(), decoded.FirstDeployed.Unix()) + assert.Equal(t, original.LastDeployed.Unix(), decoded.LastDeployed.Unix()) + assert.Equal(t, original.Deleted.Unix(), decoded.Deleted.Unix()) + assert.Equal(t, original.Description, decoded.Description) + assert.Equal(t, original.Status, decoded.Status) + assert.Equal(t, original.Notes, decoded.Notes) +} + +func TestInfoEmptyStringRoundTrip(t *testing.T) { + // This test specifically verifies that empty string time fields + // are handled correctly during parsing + input := `{"first_deployed":"","last_deployed":"","deleted":"","status":"deployed","description":"test"}` + + var info Info + err := json.Unmarshal([]byte(input), &info) + require.NoError(t, err) + + // Verify time fields are zero values + assert.True(t, info.FirstDeployed.IsZero()) + assert.True(t, info.LastDeployed.IsZero()) + assert.True(t, info.Deleted.IsZero()) + assert.Equal(t, common.StatusDeployed, info.Status) + assert.Equal(t, "test", info.Description) + + // Marshal back and verify empty time fields are omitted + data, err := json.Marshal(&info) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Zero time values should be omitted due to omitzero tag + assert.NotContains(t, result, "first_deployed") + assert.NotContains(t, result, "last_deployed") + assert.NotContains(t, result, "deleted") + assert.Equal(t, "deployed", result["status"]) + assert.Equal(t, "test", result["description"]) +} diff --git a/pkg/helm/pkg/release/v1/mock.go b/pkg/helm/pkg/release/v1/mock.go new file mode 100644 index 00000000..b5642784 --- /dev/null +++ b/pkg/helm/pkg/release/v1/mock.go @@ -0,0 +1,142 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "fmt" + "math/rand" + "time" + + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + rcommon "github.com/werf/nelm/pkg/helm/pkg/release/common" +) + +// MockHookTemplate is the hook template used for all mock release objects. +var MockHookTemplate = `apiVersion: v1 +kind: Job +metadata: + annotations: + "helm.sh/hook": pre-install +` + +// MockManifest is the manifest used for all mock release objects. +var MockManifest = `apiVersion: v1 +kind: Secret +metadata: + name: fixture +` + +// MockReleaseOptions allows for user-configurable options on mock release objects. +type MockReleaseOptions struct { + Name string + Version int + Chart *chart.Chart + Status rcommon.Status + Namespace string + Labels map[string]string +} + +// Mock creates a mock release object based on options set by MockReleaseOptions. This function should typically not be used outside of testing. +func Mock(opts *MockReleaseOptions) *Release { + date := time.Unix(242085845, 0).UTC() + + name := opts.Name + if name == "" { + name = "testrelease-" + fmt.Sprint(rand.Intn(100)) + } + + version := 1 + if opts.Version != 0 { + version = opts.Version + } + + namespace := opts.Namespace + if namespace == "" { + namespace = "default" + } + var labels map[string]string + if len(opts.Labels) > 0 { + labels = opts.Labels + } + + ch := opts.Chart + if opts.Chart == nil { + ch = &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "foo", + Version: "0.1.0-beta.1", + AppVersion: "1.0", + Annotations: map[string]string{ + "category": "web-apps", + "supported": "true", + }, + Dependencies: []*chart.Dependency{ + { + Name: "cool-plugin", + Version: "1.0.0", + Repository: "https://coolplugin.io/charts", + Condition: "coolPlugin.enabled", + Enabled: true, + }, + { + Name: "crds", + Version: "2.7.1", + Condition: "crds.enabled", + }, + }, + }, + Templates: []*common.File{ + {Name: "templates/foo.tpl", ModTime: time.Now(), Data: []byte(MockManifest)}, + }, + } + } + + scode := rcommon.StatusDeployed + if len(opts.Status) > 0 { + scode = opts.Status + } + + info := &Info{ + FirstDeployed: date, + LastDeployed: date, + Status: scode, + Description: "Release mock", + Notes: "Some mock release notes!", + } + + return &Release{ + Name: name, + Info: info, + Chart: ch, + Config: map[string]interface{}{"name": "value"}, + Version: version, + Namespace: namespace, + Hooks: []*Hook{ + { + Name: "pre-install-hook", + Kind: "Job", + Path: "pre-install-hook.yaml", + Manifest: MockHookTemplate, + LastRun: HookExecution{}, + Events: []HookEvent{HookPreInstall}, + }, + }, + Manifest: MockManifest, + Labels: labels, + } +} diff --git a/pkg/helm/pkg/release/v1/release.go b/pkg/helm/pkg/release/v1/release.go new file mode 100644 index 00000000..fc027cbc --- /dev/null +++ b/pkg/helm/pkg/release/v1/release.go @@ -0,0 +1,61 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/release/common" +) + +type ApplyMethod string + +const ApplyMethodClientSideApply ApplyMethod = "csa" +const ApplyMethodServerSideApply ApplyMethod = "ssa" + +// Release describes a deployment of a chart, together with the chart +// and the variables used to deploy that chart. +type Release struct { + // Name is the name of the release + Name string `json:"name,omitempty"` + // Info provides information about a release + Info *Info `json:"info,omitempty"` + // Chart is the chart that was released. + Chart *chart.Chart `json:"chart,omitempty"` + // Config is the set of extra Values added to the chart. + // These values override the default values inside of the chart. + Config map[string]interface{} `json:"config,omitempty"` + // Manifest is the string representation of the rendered template. + Manifest string `json:"manifest,omitempty"` + // Hooks are all of the hooks declared for this release. + Hooks []*Hook `json:"hooks,omitempty"` + // Version is an int which represents the revision of the release. + Version int `json:"version,omitempty"` + // Namespace is the kubernetes namespace of the release. + Namespace string `json:"namespace,omitempty"` + // Labels of the release. + // Disabled encoding into Json cause labels are stored in storage driver metadata field. + Labels map[string]string `json:"-"` + // ApplyMethod stores whether server-side or client-side apply was used for the release + // Unset (empty string) should be treated as the default of client-side apply + ApplyMethod string `json:"apply_method,omitempty"` // "ssa" | "csa" + UnstoredManifest string `json:"unstored_manifest,omitempty"` +} + +// SetStatus is a helper for setting the status on a release. +func (r *Release) SetStatus(status common.Status, msg string) { + r.Info.Status = status + r.Info.Description = msg +} diff --git a/pkg/helm/pkg/releaseutil/filter.go b/pkg/helm/pkg/release/v1/util/filter.go similarity index 88% rename from pkg/helm/pkg/releaseutil/filter.go rename to pkg/helm/pkg/release/v1/util/filter.go index 360d52b8..60d95031 100644 --- a/pkg/helm/pkg/releaseutil/filter.go +++ b/pkg/helm/pkg/release/v1/util/filter.go @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" +package util // import "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" -import rspb "github.com/werf/nelm/pkg/helm/pkg/release" +import ( + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) // FilterFunc returns true if the release object satisfies // the predicate of the underlying filter func. @@ -68,7 +71,7 @@ func All(filters ...FilterFunc) FilterFunc { } // StatusFilter filters a set of releases by status code. -func StatusFilter(status rspb.Status) FilterFunc { +func StatusFilter(status common.Status) FilterFunc { return FilterFunc(func(rls *rspb.Release) bool { if rls == nil { return true diff --git a/pkg/helm/pkg/release/v1/util/filter_test.go b/pkg/helm/pkg/release/v1/util/filter_test.go new file mode 100644 index 00000000..7f081e2e --- /dev/null +++ b/pkg/helm/pkg/release/v1/util/filter_test.go @@ -0,0 +1,60 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util // import "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" + +import ( + "testing" + + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +func TestFilterAny(t *testing.T) { + ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases) + if len(ls) != 2 { + t.Fatalf("expected 2 results, got '%d'", len(ls)) + } + + r0, r1 := ls[0], ls[1] + switch { + case r0.Info.Status != common.StatusUninstalled: + t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) + case r1.Info.Status != common.StatusUninstalled: + t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) + } +} + +func TestFilterAll(t *testing.T) { + fn := FilterFunc(func(rls *rspb.Release) bool { + // true if not uninstalled and version < 4 + v0 := !StatusFilter(common.StatusUninstalled).Check(rls) + v1 := rls.Version < 4 + return v0 && v1 + }) + + ls := All(fn).Filter(releases) + if len(ls) != 1 { + t.Fatalf("expected 1 result, got '%d'", len(ls)) + } + + switch r0 := ls[0]; { + case r0.Version == 4: + t.Fatal("got release with status revision 4") + case r0.Info.Status == common.StatusUninstalled: + t.Fatal("got release with status UNINSTALLED") + } +} diff --git a/pkg/helm/pkg/releaseutil/kind_sorter.go b/pkg/helm/pkg/release/v1/util/kind_sorter.go similarity index 92% rename from pkg/helm/pkg/releaseutil/kind_sorter.go rename to pkg/helm/pkg/release/v1/util/kind_sorter.go index 2455c51a..019228ae 100644 --- a/pkg/helm/pkg/releaseutil/kind_sorter.go +++ b/pkg/helm/pkg/release/v1/util/kind_sorter.go @@ -14,12 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil +package util import ( "sort" - "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) // KindSortOrder is an ordering of Kinds. @@ -65,12 +65,17 @@ var InstallOrder KindSortOrder = []string{ "IngressClass", "Ingress", "APIService", + "MutatingWebhookConfiguration", + "ValidatingWebhookConfiguration", } // UninstallOrder is the order in which manifests should be uninstalled (by Kind). // // Those occurring earlier in the list get uninstalled before those occurring later in the list. var UninstallOrder KindSortOrder = []string{ + // For uninstall, we remove validation before mutation to ensure webhooks don't block removal + "ValidatingWebhookConfiguration", + "MutatingWebhookConfiguration", "APIService", "Ingress", "IngressClass", diff --git a/pkg/helm/pkg/releaseutil/kind_sorter_test.go b/pkg/helm/pkg/release/v1/util/kind_sorter_test.go similarity index 95% rename from pkg/helm/pkg/releaseutil/kind_sorter_test.go rename to pkg/helm/pkg/release/v1/util/kind_sorter_test.go index c7dbc295..00eddfb7 100644 --- a/pkg/helm/pkg/releaseutil/kind_sorter_test.go +++ b/pkg/helm/pkg/release/v1/util/kind_sorter_test.go @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil +package util import ( "bytes" "testing" - "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) func TestKindSorter(t *testing.T) { @@ -173,6 +173,14 @@ func TestKindSorter(t *testing.T) { Name: "F", Head: &SimpleHead{Kind: "PriorityClass"}, }, + { + Name: "M", + Head: &SimpleHead{Kind: "MutatingWebhookConfiguration"}, + }, + { + Name: "V", + Head: &SimpleHead{Kind: "ValidatingWebhookConfiguration"}, + }, } for _, test := range []struct { @@ -180,8 +188,8 @@ func TestKindSorter(t *testing.T) { order KindSortOrder expected string }{ - {"install", InstallOrder, "FaAbcC3deEf1gh2iIjJkKlLmnopqrxstuUvw!"}, - {"uninstall", UninstallOrder, "wvUmutsxrqponLlKkJjIi2hg1fEed3CcbAaF!"}, + {"install", InstallOrder, "FaAbcC3deEf1gh2iIjJkKlLmnopqrxstuUvwMV!"}, + {"uninstall", UninstallOrder, "VMwvUmutsxrqponLlKkJjIi2hg1fEed3CcbAaF!"}, } { var buf bytes.Buffer t.Run(test.description, func(t *testing.T) { diff --git a/pkg/helm/pkg/release/v1/util/manifest.go b/pkg/helm/pkg/release/v1/util/manifest.go new file mode 100644 index 00000000..9a87949f --- /dev/null +++ b/pkg/helm/pkg/release/v1/util/manifest.go @@ -0,0 +1,72 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util + +import ( + "fmt" + "regexp" + "strconv" + "strings" +) + +// SimpleHead defines what the structure of the head of a manifest file +type SimpleHead struct { + Version string `json:"apiVersion"` + Kind string `json:"kind,omitempty"` + Metadata *struct { + Name string `json:"name"` + Annotations map[string]string `json:"annotations"` + } `json:"metadata,omitempty"` +} + +var sep = regexp.MustCompile("(?:^|\\s*\n)---\\s*") + +// SplitManifests takes a string of manifest and returns a map contains individual manifests +func SplitManifests(bigFile string) map[string]string { + // Basically, we're quickly splitting a stream of YAML documents into an + // array of YAML docs. The file name is just a place holder, but should be + // integer-sortable so that manifests get output in the same order as the + // input (see `BySplitManifestsOrder`). + tpl := "manifest-%d" + res := map[string]string{} + // Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly. + bigFileTmp := strings.TrimSpace(bigFile) + docs := sep.Split(bigFileTmp, -1) + var count int + for _, d := range docs { + if d == "" { + continue + } + + d = strings.TrimSpace(d) + res[fmt.Sprintf(tpl, count)] = d + count = count + 1 + } + return res +} + +// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests` +type BySplitManifestsOrder []string + +func (a BySplitManifestsOrder) Len() int { return len(a) } +func (a BySplitManifestsOrder) Less(i, j int) bool { + // Split `manifest-%d` + anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0) + bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0) + return anum < bnum +} +func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } diff --git a/pkg/helm/pkg/releaseutil/manifest_sorter.go b/pkg/helm/pkg/release/v1/util/manifest_sorter.go similarity index 82% rename from pkg/helm/pkg/releaseutil/manifest_sorter.go rename to pkg/helm/pkg/release/v1/util/manifest_sorter.go index 3a658094..d239428c 100644 --- a/pkg/helm/pkg/releaseutil/manifest_sorter.go +++ b/pkg/helm/pkg/release/v1/util/manifest_sorter.go @@ -14,20 +14,20 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil +package util import ( "fmt" + "log/slog" "path" "sort" "strconv" "strings" - "github.com/pkg/errors" "sigs.k8s.io/yaml" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/chart/common" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) // Manifest represents a manifest file, which has a name and some content. @@ -41,7 +41,6 @@ type Manifest struct { type manifestFile struct { entries map[string]string path string - apis chartutil.VersionSet } // result is an intermediate structure used during sorting. @@ -75,7 +74,7 @@ var events = map[string]release.HookEvent{ // // Files that do not parse into the expected format are simply placed into a map and // returned. -func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { +func SortManifests(files map[string]string, _ common.VersionSet, ordering KindSortOrder) ([]*release.Hook, []Manifest, error) { result := &result{} var sortedFilePaths []string @@ -100,7 +99,6 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering manifestFile := &manifestFile{ entries: SplitManifests(content), path: filePath, - apis: apis, } if err := manifestFile.sort(result); err != nil { @@ -130,6 +128,14 @@ func SortManifests(files map[string]string, apis chartutil.VersionSet, ordering // metadata: // annotations: // helm.sh/hook-delete-policy: hook-succeeded +// +// To determine the policy to output logs of the hook (for Pod and Job only), it looks for a YAML structure like this: +// +// kind: Pod +// apiVersion: v1 +// metadata: +// annotations: +// helm.sh/hook-output-log-policy: hook-succeeded,hook-failed func (file *manifestFile) sort(result *result) error { // Go through manifests in order found in file (function `SplitManifests` creates integer-sortable keys) var sortedEntryKeys []string @@ -143,7 +149,7 @@ func (file *manifestFile) sort(result *result) error { var entry SimpleHead if err := yaml.Unmarshal([]byte(m), &entry); err != nil { - return errors.Wrapf(err, "YAML parse error on %s", file.path) + return fmt.Errorf("YAML parse error on %s: %w", file.path, err) } if !hasAnyAnnotation(entry) { @@ -168,29 +174,41 @@ func (file *manifestFile) sort(result *result) error { hw := calculateHookWeight(entry) h := &release.Hook{ - Name: entry.Metadata.Name, - Kind: entry.Kind, - Path: file.path, - Manifest: m, - Events: []release.HookEvent{}, - Weight: hw, - DeletePolicies: []release.HookDeletePolicy{}, + Name: entry.Metadata.Name, + Kind: entry.Kind, + Path: file.path, + Manifest: m, + Events: []release.HookEvent{}, + Weight: hw, + DeletePolicies: []release.HookDeletePolicy{}, + OutputLogPolicies: []release.HookOutputLogPolicy{}, } - for _, hookType := range strings.Split(hookTypes, ",") { + isUnknownHook := false + for hookType := range strings.SplitSeq(hookTypes, ",") { hookType = strings.ToLower(strings.TrimSpace(hookType)) e, ok := events[hookType] if !ok { - continue + isUnknownHook = true + break } h.Events = append(h.Events, e) } + if isUnknownHook { + slog.Info("skipping unknown hooks", "hookTypes", hookTypes) + continue + } + result.hooks = append(result.hooks, h) operateAnnotationValues(entry, release.HookDeleteAnnotation, func(value string) { h.DeletePolicies = append(h.DeletePolicies, release.HookDeletePolicy(value)) }) + + operateAnnotationValues(entry, release.HookOutputLogAnnotation, func(value string) { + h.OutputLogPolicies = append(h.OutputLogPolicies, release.HookOutputLogPolicy(value)) + }) } return nil @@ -218,7 +236,7 @@ func calculateHookWeight(entry SimpleHead) int { // operateAnnotationValues finds the given annotation and runs the operate function with the value of that annotation func operateAnnotationValues(entry SimpleHead, annotation string, operate func(p string)) { if dps, ok := entry.Metadata.Annotations[annotation]; ok { - for _, dp := range strings.Split(dps, ",") { + for dp := range strings.SplitSeq(dps, ",") { dp = strings.ToLower(strings.TrimSpace(dp)) operate(dp) } diff --git a/pkg/helm/pkg/releaseutil/manifest_sorter_test.go b/pkg/helm/pkg/release/v1/util/manifest_sorter_test.go similarity index 96% rename from pkg/helm/pkg/releaseutil/manifest_sorter_test.go rename to pkg/helm/pkg/release/v1/util/manifest_sorter_test.go index 48db96ad..34cf18cb 100644 --- a/pkg/helm/pkg/releaseutil/manifest_sorter_test.go +++ b/pkg/helm/pkg/release/v1/util/manifest_sorter_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil +package util import ( "reflect" @@ -22,8 +22,7 @@ import ( "sigs.k8s.io/yaml" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - "github.com/werf/nelm/pkg/helm/pkg/release" + release "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) func TestSortManifests(t *testing.T) { @@ -139,7 +138,7 @@ metadata: manifests[o.path] = o.manifest } - hs, generic, err := SortManifests(manifests, chartutil.VersionSet{"v1", "v1beta1"}, InstallOrder) + hs, generic, err := SortManifests(manifests, nil, InstallOrder) if err != nil { t.Fatalf("Unexpected error: %s", err) } diff --git a/pkg/helm/pkg/releaseutil/manifest_test.go b/pkg/helm/pkg/release/v1/util/manifest_test.go similarity index 95% rename from pkg/helm/pkg/releaseutil/manifest_test.go rename to pkg/helm/pkg/release/v1/util/manifest_test.go index 8664d20e..754ac136 100644 --- a/pkg/helm/pkg/releaseutil/manifest_test.go +++ b/pkg/helm/pkg/release/v1/util/manifest_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" +package util // import "helm.sh/helm/v4/pkg/release/v1/util" import ( "reflect" diff --git a/pkg/helm/pkg/release/v1/util/sorter.go b/pkg/helm/pkg/release/v1/util/sorter.go new file mode 100644 index 00000000..20533391 --- /dev/null +++ b/pkg/helm/pkg/release/v1/util/sorter.go @@ -0,0 +1,61 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package util // import "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" + +import ( + "sort" + + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" +) + +// Reverse reverses the list of releases sorted by the sort func. +func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) { + sortFn(list) + for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { + list[i], list[j] = list[j], list[i] + } +} + +// SortByName returns the list of releases sorted +// in lexicographical order. +func SortByName(list []*rspb.Release) { + sort.Slice(list, func(i, j int) bool { + return list[i].Name < list[j].Name + }) +} + +// SortByDate returns the list of releases sorted by a +// release's last deployed time (in seconds). +func SortByDate(list []*rspb.Release) { + sort.Slice(list, func(i, j int) bool { + ti := list[i].Info.LastDeployed.Unix() + tj := list[j].Info.LastDeployed.Unix() + if ti != tj { + return ti < tj + } + // Use name as tie-breaker for stable sorting + return list[i].Name < list[j].Name + }) +} + +// SortByRevision returns the list of releases sorted by a +// release's revision number (release.Version). +func SortByRevision(list []*rspb.Release) { + sort.Slice(list, func(i, j int) bool { + return list[i].Version < list[j].Version + }) +} diff --git a/pkg/helm/pkg/releaseutil/sorter_test.go b/pkg/helm/pkg/release/v1/util/sorter_test.go similarity index 80% rename from pkg/helm/pkg/releaseutil/sorter_test.go rename to pkg/helm/pkg/release/v1/util/sorter_test.go index ec03abcb..7e4301e1 100644 --- a/pkg/helm/pkg/releaseutil/sorter_test.go +++ b/pkg/helm/pkg/release/v1/util/sorter_test.go @@ -14,27 +14,27 @@ See the License for the specific language governing permissions and limitations under the License. */ -package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" +package util // import "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" import ( "testing" "time" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) // note: this test data is shared with filter_test.go. var releases = []*rspb.Release{ - tsRelease("quiet-bear", 2, 2000, rspb.StatusSuperseded), - tsRelease("angry-bird", 4, 3000, rspb.StatusDeployed), - tsRelease("happy-cats", 1, 4000, rspb.StatusUninstalled), - tsRelease("vocal-dogs", 3, 6000, rspb.StatusUninstalled), + tsRelease("quiet-bear", 2, 2000, common.StatusSuperseded), + tsRelease("angry-bird", 4, 3000, common.StatusDeployed), + tsRelease("happy-cats", 1, 4000, common.StatusUninstalled), + tsRelease("vocal-dogs", 3, 6000, common.StatusUninstalled), } -func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { - info := &rspb.Info{Status: status, LastDeployed: helmtime.Now().Add(dur)} +func tsRelease(name string, vers int, dur time.Duration, status common.Status) *rspb.Release { + info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)} return &rspb.Release{ Name: name, Version: vers, @@ -43,6 +43,7 @@ func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rs } func check(t *testing.T, by string, fn func(int, int) bool) { + t.Helper() for i := len(releases) - 1; i > 0; i-- { if fn(i, i-1) { t.Errorf("release at positions '(%d,%d)' not sorted by %s", i-1, i, by) diff --git a/pkg/helm/pkg/releaseutil/filter_test.go b/pkg/helm/pkg/releaseutil/filter_test.go deleted file mode 100644 index 4e287bfb..00000000 --- a/pkg/helm/pkg/releaseutil/filter_test.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" - -import ( - "testing" - - rspb "github.com/werf/nelm/pkg/helm/pkg/release" -) - -func TestFilterAny(t *testing.T) { - ls := Any(StatusFilter(rspb.StatusUninstalled)).Filter(releases) - if len(ls) != 2 { - t.Fatalf("expected 2 results, got '%d'", len(ls)) - } - - r0, r1 := ls[0], ls[1] - switch { - case r0.Info.Status != rspb.StatusUninstalled: - t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - case r1.Info.Status != rspb.StatusUninstalled: - t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - } -} - -func TestFilterAll(t *testing.T) { - fn := FilterFunc(func(rls *rspb.Release) bool { - // true if not uninstalled and version < 4 - v0 := !StatusFilter(rspb.StatusUninstalled).Check(rls) - v1 := rls.Version < 4 - return v0 && v1 - }) - - ls := All(fn).Filter(releases) - if len(ls) != 1 { - t.Fatalf("expected 1 result, got '%d'", len(ls)) - } - - switch r0 := ls[0]; { - case r0.Version == 4: - t.Fatal("got release with status revision 4") - case r0.Info.Status == rspb.StatusUninstalled: - t.Fatal("got release with status UNINSTALLED") - } -} diff --git a/pkg/helm/pkg/releaseutil/manifest.go b/pkg/helm/pkg/releaseutil/manifest.go deleted file mode 100644 index 3962a709..00000000 --- a/pkg/helm/pkg/releaseutil/manifest.go +++ /dev/null @@ -1,102 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package releaseutil - -import ( - "fmt" - "regexp" - "sort" - "strconv" - "strings" - - "github.com/samber/lo" -) - -// SimpleHead defines what the structure of the head of a manifest file -type SimpleHead struct { - Version string `json:"apiVersion"` - Kind string `json:"kind,omitempty"` - Metadata *struct { - Name string `json:"name"` - Annotations map[string]string `json:"annotations"` - } `json:"metadata,omitempty"` -} - -var sep = regexp.MustCompile(`(?m)^---\s*`) - -// SplitManifests takes a string of manifest and returns a map contains individual manifests -func SplitManifests(bigFile string) map[string]string { - // Basically, we're quickly splitting a stream of YAML documents into an - // array of YAML docs. The file name is just a place holder, but should be - // integer-sortable so that manifests get output in the same order as the - // input (see `BySplitManifestsOrder`). - tpl := "manifest-%d" - res := map[string]string{} - // Making sure that any extra whitespace in YAML stream doesn't interfere in splitting documents correctly. - bigFileTmp := strings.TrimSpace(bigFile) - docs := sep.Split(bigFileTmp, -1) - var count int - for _, d := range docs { - d = strings.TrimSpace(d) - - if d == "" { - continue - } - - var contentFound bool - for _, line := range strings.Split(d, "\n") { - trimmedLine := strings.TrimSpace(line) - if trimmedLine != "" && !strings.HasPrefix(trimmedLine, "#") { - contentFound = true - break - } - } - - if !contentFound { - continue - } - - d += "\n" - - res[fmt.Sprintf(tpl, count)] = d - count = count + 1 - } - return res -} - -// BySplitManifestsOrder sorts by in-file manifest order, as provided in function `SplitManifests` -type BySplitManifestsOrder []string - -func (a BySplitManifestsOrder) Len() int { return len(a) } -func (a BySplitManifestsOrder) Less(i, j int) bool { - // Split `manifest-%d` - anum, _ := strconv.ParseInt(a[i][len("manifest-"):], 10, 0) - bnum, _ := strconv.ParseInt(a[j][len("manifest-"):], 10, 0) - return anum < bnum -} -func (a BySplitManifestsOrder) Swap(i, j int) { a[i], a[j] = a[j], a[i] } - -func SplitManifestsToSlice(manifests string) []string { - splitManifests := SplitManifests(manifests) - - keys := lo.Keys(splitManifests) - sort.Strings(keys) - - return lo.Map(keys, func(k string, _ int) string { - return splitManifests[k] - }) -} diff --git a/pkg/helm/pkg/releaseutil/sorter.go b/pkg/helm/pkg/releaseutil/sorter.go deleted file mode 100644 index 0c2535fe..00000000 --- a/pkg/helm/pkg/releaseutil/sorter.go +++ /dev/null @@ -1,78 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package releaseutil // import "helm.sh/helm/v3/pkg/releaseutil" - -import ( - "sort" - - rspb "github.com/werf/nelm/pkg/helm/pkg/release" -) - -type list []*rspb.Release - -func (s list) Len() int { return len(s) } -func (s list) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// ByName sorts releases by name -type ByName struct{ list } - -// Less compares to releases -func (s ByName) Less(i, j int) bool { return s.list[i].Name < s.list[j].Name } - -// ByDate sorts releases by date -type ByDate struct{ list } - -// Less compares to releases -func (s ByDate) Less(i, j int) bool { - ti := s.list[i].Info.LastDeployed.Unix() - tj := s.list[j].Info.LastDeployed.Unix() - return ti < tj -} - -// ByRevision sorts releases by revision number -type ByRevision struct{ list } - -// Less compares to releases -func (s ByRevision) Less(i, j int) bool { - return s.list[i].Version < s.list[j].Version -} - -// Reverse reverses the list of releases sorted by the sort func. -func Reverse(list []*rspb.Release, sortFn func([]*rspb.Release)) { - sortFn(list) - for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 { - list[i], list[j] = list[j], list[i] - } -} - -// SortByName returns the list of releases sorted -// in lexicographical order. -func SortByName(list []*rspb.Release) { - sort.Sort(ByName{list}) -} - -// SortByDate returns the list of releases sorted by a -// release's last deployed time (in seconds). -func SortByDate(list []*rspb.Release) { - sort.Sort(ByDate{list}) -} - -// SortByRevision returns the list of releases sorted by a -// release's revision number (release.Version). -func SortByRevision(list []*rspb.Release) { - sort.Sort(ByRevision{list}) -} diff --git a/pkg/helm/pkg/releaseutil/validate.go b/pkg/helm/pkg/releaseutil/validate.go deleted file mode 100644 index d6053e7d..00000000 --- a/pkg/helm/pkg/releaseutil/validate.go +++ /dev/null @@ -1,152 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package releaseutil - -import ( - "fmt" - - "github.com/pkg/errors" - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/cli-runtime/pkg/resource" -) - -var accessor = meta.NewAccessor() - -const ( - appManagedByLabel = "app.kubernetes.io/managed-by" - appManagedByHelm = "Helm" - helmReleaseNameAnnotation = "meta.helm.sh/release-name" - helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace" -) - -func CheckOwnership(obj runtime.Object, releaseName, releaseNamespace string) error { - lbls, err := accessor.Labels(obj) - if err != nil { - return err - } - annos, err := accessor.Annotations(obj) - if err != nil { - return err - } - - var errs []error - if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil { - errs = append(errs, fmt.Errorf("label validation error: %s", err)) - } - if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil { - errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) - } - if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil { - errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) - } - - if len(errs) > 0 { - err := errors.New("invalid ownership metadata") - for _, e := range errs { - err = fmt.Errorf("%w; %s", err, e) - } - return err - } - - return nil -} - -func requireValue(meta map[string]string, k, v string) error { - actual, ok := meta[k] - if !ok { - return fmt.Errorf("missing key %q: must be set to %q", k, v) - } - if actual != v { - return fmt.Errorf("key %q must equal %q: current value is %q", k, v, actual) - } - return nil -} - -// SetMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing -// ownership metadata will be overwritten. Otherwise an error will be returned if any resource has an -// existing and conflicting value for the managed by label or Helm release/namespace annotations. -func SetMetadataVisitor(releaseName, releaseNamespace string, force bool) resource.VisitorFunc { - return func(info *resource.Info, err error) error { - if err != nil { - return err - } - - if !force { - if err := CheckOwnership(info.Object, releaseName, releaseNamespace); err != nil { - return fmt.Errorf("%s cannot be owned: %s", ResourceString(info), err) - } - } - - if err := mergeLabels(info.Object, map[string]string{ - appManagedByLabel: appManagedByHelm, - }); err != nil { - return fmt.Errorf( - "%s labels could not be updated: %s", - ResourceString(info), err, - ) - } - - if err := mergeAnnotations(info.Object, map[string]string{ - helmReleaseNameAnnotation: releaseName, - helmReleaseNamespaceAnnotation: releaseNamespace, - }); err != nil { - return fmt.Errorf( - "%s annotations could not be updated: %s", - ResourceString(info), err, - ) - } - - return nil - } -} - -func ResourceString(info *resource.Info) string { - _, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind() - return fmt.Sprintf( - "%s %q in namespace %q", - k, info.Name, info.Namespace, - ) -} - -func mergeLabels(obj runtime.Object, labels map[string]string) error { - current, err := accessor.Labels(obj) - if err != nil { - return err - } - return accessor.SetLabels(obj, mergeStrStrMaps(current, labels)) -} - -func mergeAnnotations(obj runtime.Object, annotations map[string]string) error { - current, err := accessor.Annotations(obj) - if err != nil { - return err - } - return accessor.SetAnnotations(obj, mergeStrStrMaps(current, annotations)) -} - -// merge two maps, always taking the value on the right -func mergeStrStrMaps(current, desired map[string]string) map[string]string { - result := make(map[string]string) - for k, v := range current { - result[k] = v - } - for k, desiredVal := range desired { - result[k] = desiredVal - } - return result -} diff --git a/pkg/helm/pkg/releaseutil/validate_test.go b/pkg/helm/pkg/releaseutil/validate_test.go deleted file mode 100644 index 49286d42..00000000 --- a/pkg/helm/pkg/releaseutil/validate_test.go +++ /dev/null @@ -1,89 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package releaseutil - -import ( - "testing" - - "github.com/stretchr/testify/assert" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/api/meta" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" -) - -func newDeploymentResource(name, namespace string) *resource.Info { - return &resource.Info{ - Name: name, - Mapping: &meta.RESTMapping{ - Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, - GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, - }, - Object: &appsv1.Deployment{ - ObjectMeta: v1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - }, - } -} - -func TestCheckOwnership(t *testing.T) { - deployFoo := newDeploymentResource("foo", "ns-a") - - // Verify that a resource that lacks labels/annotations is not owned - err := CheckOwnership(deployFoo.Object, "rel-a", "ns-a") - assert.EqualError(t, err, `invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) - - // Set managed by label and verify annotation error message - _ = accessor.SetLabels(deployFoo.Object, map[string]string{ - appManagedByLabel: appManagedByHelm, - }) - err = CheckOwnership(deployFoo.Object, "rel-a", "ns-a") - assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) - - // Set only the release name annotation and verify missing release namespace error message - _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ - helmReleaseNameAnnotation: "rel-a", - }) - err = CheckOwnership(deployFoo.Object, "rel-a", "ns-a") - assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) - - // Set both release name and namespace annotations and verify no ownership errors - _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ - helmReleaseNameAnnotation: "rel-a", - helmReleaseNamespaceAnnotation: "ns-a", - }) - err = CheckOwnership(deployFoo.Object, "rel-a", "ns-a") - assert.NoError(t, err) - - // Verify ownership error for wrong release name - err = CheckOwnership(deployFoo.Object, "rel-b", "ns-a") - assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-name" must equal "rel-b": current value is "rel-a"`) - - // Verify ownership error for wrong release namespace - err = CheckOwnership(deployFoo.Object, "rel-a", "ns-b") - assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "ns-b": current value is "ns-a"`) - - // Verify ownership error for wrong manager label - _ = accessor.SetLabels(deployFoo.Object, map[string]string{ - appManagedByLabel: "helm", - }) - err = CheckOwnership(deployFoo.Object, "rel-a", "ns-a") - assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`) -} diff --git a/pkg/helm/pkg/repo/chartrepo.go b/pkg/helm/pkg/repo/chartrepo.go deleted file mode 100644 index c88bb119..00000000 --- a/pkg/helm/pkg/repo/chartrepo.go +++ /dev/null @@ -1,318 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package repo // import "helm.sh/helm/v3/pkg/repo" - -import ( - "crypto/rand" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "log" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/getter" - "github.com/werf/nelm/pkg/helm/pkg/helmpath" - "github.com/werf/nelm/pkg/helm/pkg/provenance" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" -) - -// Entry represents a collection of parameters for chart repository -type Entry struct { - Name string `json:"name"` - URL string `json:"url"` - Username string `json:"username"` - Password string `json:"password"` - CertFile string `json:"certFile"` - KeyFile string `json:"keyFile"` - CAFile string `json:"caFile"` - InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"` - PassCredentialsAll bool `json:"pass_credentials_all"` -} - -// ChartRepository represents a chart repository -type ChartRepository struct { - Config *Entry - ChartPaths []string - IndexFile *IndexFile - Client getter.Getter - CachePath string -} - -// NewChartRepository constructs ChartRepository -func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) { - u, err := url.Parse(cfg.URL) - if err != nil { - return nil, errors.Errorf("invalid chart URL format: %s", cfg.URL) - } - - client, err := getters.ByScheme(u.Scheme) - if err != nil { - return nil, errors.Errorf("could not find protocol handler for: %s", u.Scheme) - } - - return &ChartRepository{ - Config: cfg, - IndexFile: NewIndexFile(), - Client: client, - CachePath: helmpath.CachePath("repository"), - }, nil -} - -// Load loads a directory of charts as if it were a repository. -// -// It requires the presence of an index.yaml file in the directory. -// -// Deprecated: remove in Helm 4. -func (r *ChartRepository) Load() error { - dirInfo, err := os.Stat(r.Config.Name) - if err != nil { - return err - } - if !dirInfo.IsDir() { - return errors.Errorf("%q is not a directory", r.Config.Name) - } - - // FIXME: Why are we recursively walking directories? - // FIXME: Why are we not reading the repositories.yaml to figure out - // what repos to use? - filepath.Walk(r.Config.Name, func(path string, f os.FileInfo, err error) error { - if !f.IsDir() { - if strings.Contains(f.Name(), "-index.yaml") { - i, err := LoadIndexFile(path) - if err != nil { - return err - } - r.IndexFile = i - } else if strings.HasSuffix(f.Name(), ".tgz") { - r.ChartPaths = append(r.ChartPaths, path) - } - } - return nil - }) - return nil -} - -// DownloadIndexFile fetches the index from a repository. -func (r *ChartRepository) DownloadIndexFile() (string, error) { - indexURL, err := ResolveReferenceURL(r.Config.URL, "index.yaml") - if err != nil { - return "", err - } - - resp, err := r.Client.Get(indexURL, - getter.WithURL(r.Config.URL), - getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify), - getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile), - getter.WithBasicAuth(r.Config.Username, r.Config.Password), - getter.WithPassCredentialsAll(r.Config.PassCredentialsAll), - ) - if err != nil { - return "", err - } - - index, err := io.ReadAll(resp) - if err != nil { - return "", err - } - - indexFile, err := loadIndex(index, r.Config.URL) - if err != nil { - return "", err - } - - // Create the chart list file in the cache directory - var charts strings.Builder - for name := range indexFile.Entries { - fmt.Fprintln(&charts, name) - } - chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) - os.MkdirAll(filepath.Dir(chartsFile), 0755) - os.WriteFile(chartsFile, []byte(charts.String()), 0644) - - // Create the index file in the cache directory - fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)) - os.MkdirAll(filepath.Dir(fname), 0755) - return fname, os.WriteFile(fname, index, 0644) -} - -// Index generates an index for the chart repository and writes an index.yaml file. -func (r *ChartRepository) Index(opts helmopts.HelmOptions) error { - err := r.generateIndex(opts) - if err != nil { - return err - } - return r.saveIndexFile() -} - -func (r *ChartRepository) saveIndexFile() error { - index, err := yaml.Marshal(r.IndexFile) - if err != nil { - return err - } - return os.WriteFile(filepath.Join(r.Config.Name, indexPath), index, 0644) -} - -func (r *ChartRepository) generateIndex(opts helmopts.HelmOptions) error { - for _, path := range r.ChartPaths { - ch, err := loader.Load(path, opts) - if err != nil { - return err - } - - digest, err := provenance.DigestFile(path) - if err != nil { - return err - } - - if !r.IndexFile.Has(ch.Name(), ch.Metadata.Version) { - if err := r.IndexFile.MustAdd(ch.Metadata, path, r.Config.URL, digest); err != nil { - return errors.Wrapf(err, "failed adding to %s to index", path) - } - } - // TODO: If a chart exists, but has a different Digest, should we error? - } - r.IndexFile.SortEntries() - return nil -} - -// FindChartInRepoURL finds chart in chart repository pointed by repoURL -// without adding repo to repositories -func FindChartInRepoURL(repoURL, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) { - return FindChartInAuthRepoURL(repoURL, "", "", chartName, chartVersion, certFile, keyFile, caFile, getters) -} - -// FindChartInAuthRepoURL finds chart in chart repository pointed by repoURL -// without adding repo to repositories, like FindChartInRepoURL, -// but it also receives credentials for the chart repository. -func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) { - return FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, getters) -} - -// FindChartInAuthAndTLSRepoURL finds chart in chart repository pointed by repoURL -// without adding repo to repositories, like FindChartInRepoURL, -// but it also receives credentials and TLS verify flag for the chart repository. -// TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL. -func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) { - return FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, insecureSkipTLSverify, false, getters) -} - -// FindChartInAuthAndTLSAndPassRepoURL finds chart in chart repository pointed by repoURL -// without adding repo to repositories, like FindChartInRepoURL, -// but it also receives credentials, TLS verify flag, and if credentials should -// be passed on to other domains. -// TODO Helm 4, FindChartInAuthAndTLSAndPassRepoURL should be integrated into FindChartInAuthRepoURL. -func FindChartInAuthAndTLSAndPassRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify, passCredentialsAll bool, getters getter.Providers) (string, error) { - - // Download and write the index file to a temporary location - buf := make([]byte, 20) - rand.Read(buf) - name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-") - - c := Entry{ - URL: repoURL, - Username: username, - Password: password, - PassCredentialsAll: passCredentialsAll, - CertFile: certFile, - KeyFile: keyFile, - CAFile: caFile, - Name: name, - InsecureSkipTLSverify: insecureSkipTLSverify, - } - r, err := NewChartRepository(&c, getters) - if err != nil { - return "", err - } - idx, err := r.DownloadIndexFile() - if err != nil { - return "", errors.Wrapf(err, "looks like %q is not a valid chart repository or cannot be reached", repoURL) - } - defer func() { - os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))) - os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))) - }() - - // Read the index file for the repository to get chart information and return chart URL - repoIndex, err := LoadIndexFile(idx) - if err != nil { - return "", err - } - - errMsg := fmt.Sprintf("chart %q", chartName) - if chartVersion != "" { - errMsg = fmt.Sprintf("%s version %q", errMsg, chartVersion) - } - cv, err := repoIndex.Get(chartName, chartVersion) - if err != nil { - return "", errors.Errorf("%s not found in %s repository", errMsg, repoURL) - } - - if len(cv.URLs) == 0 { - return "", errors.Errorf("%s has no downloadable URLs", errMsg) - } - - chartURL := cv.URLs[0] - - absoluteChartURL, err := ResolveReferenceURL(repoURL, chartURL) - if err != nil { - return "", errors.Wrap(err, "failed to make chart URL absolute") - } - - return absoluteChartURL, nil -} - -// ResolveReferenceURL resolves refURL relative to baseURL. -// If refURL is absolute, it simply returns refURL. -func ResolveReferenceURL(baseURL, refURL string) (string, error) { - parsedRefURL, err := url.Parse(refURL) - if err != nil { - return "", errors.Wrapf(err, "failed to parse %s as URL", refURL) - } - - if parsedRefURL.IsAbs() { - return refURL, nil - } - - parsedBaseURL, err := url.Parse(baseURL) - if err != nil { - return "", errors.Wrapf(err, "failed to parse %s as URL", baseURL) - } - - // We need a trailing slash for ResolveReference to work, but make sure there isn't already one - parsedBaseURL.RawPath = strings.TrimSuffix(parsedBaseURL.RawPath, "/") + "/" - parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/" - - resolvedURL := parsedBaseURL.ResolveReference(parsedRefURL) - resolvedURL.RawQuery = parsedBaseURL.RawQuery - return resolvedURL.String(), nil -} - -func (e *Entry) String() string { - buf, err := json.Marshal(e) - if err != nil { - log.Panic(err) - } - return string(buf) -} diff --git a/pkg/helm/pkg/repo/chartrepo_test.go b/pkg/helm/pkg/repo/chartrepo_test.go deleted file mode 100644 index 615ad6c6..00000000 --- a/pkg/helm/pkg/repo/chartrepo_test.go +++ /dev/null @@ -1,402 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package repo - -import ( - "bytes" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "reflect" - "runtime" - "strings" - "testing" - "time" - - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/cli" - "github.com/werf/nelm/pkg/helm/pkg/getter" -) - -const ( - testRepository = "testdata/repository" - testURL = "http://example-charts.com" -) - -func TestLoadChartRepository(t *testing.T) { - r, err := NewChartRepository(&Entry{ - Name: testRepository, - URL: testURL, - }, getter.All(&cli.EnvSettings{})) - if err != nil { - t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) - } - - if err := r.Load(); err != nil { - t.Errorf("Problem loading chart repository from %s: %v", testRepository, err) - } - - paths := []string{ - filepath.Join(testRepository, "frobnitz-1.2.3.tgz"), - filepath.Join(testRepository, "sprocket-1.1.0.tgz"), - filepath.Join(testRepository, "sprocket-1.2.0.tgz"), - filepath.Join(testRepository, "universe/zarthal-1.0.0.tgz"), - } - - if r.Config.Name != testRepository { - t.Errorf("Expected %s as Name but got %s", testRepository, r.Config.Name) - } - - if !reflect.DeepEqual(r.ChartPaths, paths) { - t.Errorf("Expected %#v but got %#v\n", paths, r.ChartPaths) - } - - if r.Config.URL != testURL { - t.Errorf("Expected url for chart repository to be %s but got %s", testURL, r.Config.URL) - } -} - -func TestIndex(t *testing.T) { - r, err := NewChartRepository(&Entry{ - Name: testRepository, - URL: testURL, - }, getter.All(&cli.EnvSettings{})) - if err != nil { - t.Errorf("Problem creating chart repository from %s: %v", testRepository, err) - } - - if err := r.Load(); err != nil { - t.Errorf("Problem loading chart repository from %s: %v", testRepository, err) - } - - err = r.Index() - if err != nil { - t.Errorf("Error performing index: %v\n", err) - } - - tempIndexPath := filepath.Join(testRepository, indexPath) - actual, err := LoadIndexFile(tempIndexPath) - defer os.Remove(tempIndexPath) // clean up - if err != nil { - t.Errorf("Error loading index file %v", err) - } - verifyIndex(t, actual) - - // Re-index and test again. - err = r.Index() - if err != nil { - t.Errorf("Error performing re-index: %s\n", err) - } - second, err := LoadIndexFile(tempIndexPath) - if err != nil { - t.Errorf("Error re-loading index file %v", err) - } - verifyIndex(t, second) -} - -type CustomGetter struct { - repoUrls []string -} - -func (g *CustomGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) { - index := &IndexFile{ - APIVersion: "v1", - Generated: time.Now(), - } - indexBytes, err := yaml.Marshal(index) - if err != nil { - return nil, err - } - g.repoUrls = append(g.repoUrls, href) - return bytes.NewBuffer(indexBytes), nil -} - -func TestIndexCustomSchemeDownload(t *testing.T) { - repoName := "gcs-repo" - repoURL := "gs://some-gcs-bucket" - myCustomGetter := &CustomGetter{} - customGetterConstructor := func(options ...getter.Option) (getter.Getter, error) { - return myCustomGetter, nil - } - providers := getter.Providers{{ - Schemes: []string{"gs"}, - New: customGetterConstructor, - }} - repo, err := NewChartRepository(&Entry{ - Name: repoName, - URL: repoURL, - }, providers) - if err != nil { - t.Fatalf("Problem loading chart repository from %s: %v", repoURL, err) - } - repo.CachePath = t.TempDir() - - tempIndexFile, err := os.CreateTemp("", "test-repo") - if err != nil { - t.Fatalf("Failed to create temp index file: %v", err) - } - defer os.Remove(tempIndexFile.Name()) - - idx, err := repo.DownloadIndexFile() - if err != nil { - t.Fatalf("Failed to download index file to %s: %v", idx, err) - } - - if len(myCustomGetter.repoUrls) != 1 { - t.Fatalf("Custom Getter.Get should be called once") - } - - expectedRepoIndexURL := repoURL + "/index.yaml" - if myCustomGetter.repoUrls[0] != expectedRepoIndexURL { - t.Fatalf("Custom Getter.Get should be called with %s", expectedRepoIndexURL) - } -} - -func verifyIndex(t *testing.T, actual *IndexFile) { - var empty time.Time - if actual.Generated.Equal(empty) { - t.Errorf("Generated should be greater than 0: %s", actual.Generated) - } - - if actual.APIVersion != APIVersionV1 { - t.Error("Expected v1 API") - } - - entries := actual.Entries - if numEntries := len(entries); numEntries != 3 { - t.Errorf("Expected 3 charts to be listed in index file but got %v", numEntries) - } - - expects := map[string]ChartVersions{ - "frobnitz": { - { - Metadata: &chart.Metadata{ - Name: "frobnitz", - Version: "1.2.3", - }, - }, - }, - "sprocket": { - { - Metadata: &chart.Metadata{ - Name: "sprocket", - Version: "1.2.0", - }, - }, - { - Metadata: &chart.Metadata{ - Name: "sprocket", - Version: "1.1.0", - }, - }, - }, - "zarthal": { - { - Metadata: &chart.Metadata{ - Name: "zarthal", - Version: "1.0.0", - }, - }, - }, - } - - for name, versions := range expects { - got, ok := entries[name] - if !ok { - t.Errorf("Could not find %q entry", name) - continue - } - if len(versions) != len(got) { - t.Errorf("Expected %d versions, got %d", len(versions), len(got)) - continue - } - for i, e := range versions { - g := got[i] - if e.Name != g.Name { - t.Errorf("Expected %q, got %q", e.Name, g.Name) - } - if e.Version != g.Version { - t.Errorf("Expected %q, got %q", e.Version, g.Version) - } - if len(g.Keywords) != 3 { - t.Error("Expected 3 keywords.") - } - if len(g.Maintainers) != 2 { - t.Error("Expected 2 maintainers.") - } - if g.Created.Equal(empty) { - t.Error("Expected created to be non-empty") - } - if g.Description == "" { - t.Error("Expected description to be non-empty") - } - if g.Home == "" { - t.Error("Expected home to be non-empty") - } - if g.Digest == "" { - t.Error("Expected digest to be non-empty") - } - if len(g.URLs) != 1 { - t.Error("Expected exactly 1 URL") - } - } - } -} - -// startLocalServerForTests Start the local helm server -func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { - if handler == nil { - fileBytes, err := os.ReadFile("testdata/local-index.yaml") - if err != nil { - return nil, err - } - handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(fileBytes) - }) - } - - return httptest.NewServer(handler), nil -} - -// startLocalTLSServerForTests Start the local helm server with TLS -func startLocalTLSServerForTests(handler http.Handler) (*httptest.Server, error) { - if handler == nil { - fileBytes, err := os.ReadFile("testdata/local-index.yaml") - if err != nil { - return nil, err - } - handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Write(fileBytes) - }) - } - - return httptest.NewTLSServer(handler), nil -} - -func TestFindChartInAuthAndTLSAndPassRepoURL(t *testing.T) { - srv, err := startLocalTLSServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - chartURL, err := FindChartInAuthAndTLSAndPassRepoURL(srv.URL, "", "", "nginx", "", "", "", "", true, false, getter.All(&cli.EnvSettings{})) - if err != nil { - t.Fatalf("%v", err) - } - if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { - t.Errorf("%s is not the valid URL", chartURL) - } - - // If the insecureSkipTLsverify is false, it will return an error that contains "x509: certificate signed by unknown authority". - _, err = FindChartInAuthAndTLSAndPassRepoURL(srv.URL, "", "", "nginx", "0.1.0", "", "", "", false, false, getter.All(&cli.EnvSettings{})) - // Go communicates with the platform and different platforms return different messages. Go itself tests darwin - // differently for its message. On newer versions of Darwin the message includes the "Acme Co" portion while older - // versions of Darwin do not. As there are people developing Helm using both old and new versions of Darwin we test - // for both messages. - if runtime.GOOS == "darwin" { - if !strings.Contains(err.Error(), "x509: “Acme Co” certificate is not trusted") && !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { - t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) - } - } else if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { - t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) - } -} - -func TestFindChartInRepoURL(t *testing.T) { - srv, err := startLocalServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - chartURL, err := FindChartInRepoURL(srv.URL, "nginx", "", "", "", "", getter.All(&cli.EnvSettings{})) - if err != nil { - t.Fatalf("%v", err) - } - if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { - t.Errorf("%s is not the valid URL", chartURL) - } - - chartURL, err = FindChartInRepoURL(srv.URL, "nginx", "0.1.0", "", "", "", getter.All(&cli.EnvSettings{})) - if err != nil { - t.Errorf("%s", err) - } - if chartURL != "https://charts.helm.sh/stable/nginx-0.1.0.tgz" { - t.Errorf("%s is not the valid URL", chartURL) - } -} - -func TestErrorFindChartInRepoURL(t *testing.T) { - - g := getter.All(&cli.EnvSettings{ - RepositoryCache: t.TempDir(), - }) - - if _, err := FindChartInRepoURL("http://someserver/something", "nginx", "", "", "", "", g); err == nil { - t.Errorf("Expected error for bad chart URL, but did not get any errors") - } else if !strings.Contains(err.Error(), `looks like "http://someserver/something" is not a valid chart repository or cannot be reached`) { - t.Errorf("Expected error for bad chart URL, but got a different error (%v)", err) - } - - srv, err := startLocalServerForTests(nil) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - - if _, err = FindChartInRepoURL(srv.URL, "nginx1", "", "", "", "", g); err == nil { - t.Errorf("Expected error for chart not found, but did not get any errors") - } else if err.Error() != `chart "nginx1" not found in `+srv.URL+` repository` { - t.Errorf("Expected error for chart not found, but got a different error (%v)", err) - } - - if _, err = FindChartInRepoURL(srv.URL, "nginx1", "0.1.0", "", "", "", g); err == nil { - t.Errorf("Expected error for chart not found, but did not get any errors") - } else if err.Error() != `chart "nginx1" version "0.1.0" not found in `+srv.URL+` repository` { - t.Errorf("Expected error for chart not found, but got a different error (%v)", err) - } - - if _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", "", "", "", "", g); err == nil { - t.Errorf("Expected error for no chart URLs available, but did not get any errors") - } else if err.Error() != `chart "chartWithNoURL" has no downloadable URLs` { - t.Errorf("Expected error for chart not found, but got a different error (%v)", err) - } -} - -func TestResolveReferenceURL(t *testing.T) { - for _, tt := range []struct { - baseURL, refURL, chartURL string - }{ - {"http://localhost:8123/charts/", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz"}, - {"http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz", "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz"}, - {"http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz", "https://charts.helm.sh/stable/nginx-0.2.0.tgz"}, - {"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz", "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz"}, - {"http://localhost:8123/charts?with=queryparameter", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz?with=queryparameter"}, - } { - chartURL, err := ResolveReferenceURL(tt.baseURL, tt.refURL) - if err != nil { - t.Errorf("unexpected error in ResolveReferenceURL(%q, %q): %s", tt.baseURL, tt.refURL, err) - } - if chartURL != tt.chartURL { - t.Errorf("expected ResolveReferenceURL(%q, %q) to equal %q, got %q", tt.baseURL, tt.refURL, tt.chartURL, chartURL) - } - } -} diff --git a/pkg/helm/pkg/repo/repo.go b/pkg/helm/pkg/repo/repo.go deleted file mode 100644 index 834d554b..00000000 --- a/pkg/helm/pkg/repo/repo.go +++ /dev/null @@ -1,125 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package repo // import "helm.sh/helm/v3/pkg/repo" - -import ( - "os" - "path/filepath" - "time" - - "github.com/pkg/errors" - "sigs.k8s.io/yaml" -) - -// File represents the repositories.yaml file -type File struct { - APIVersion string `json:"apiVersion"` - Generated time.Time `json:"generated"` - Repositories []*Entry `json:"repositories"` -} - -// NewFile generates an empty repositories file. -// -// Generated and APIVersion are automatically set. -func NewFile() *File { - return &File{ - APIVersion: APIVersionV1, - Generated: time.Now(), - Repositories: []*Entry{}, - } -} - -// LoadFile takes a file at the given path and returns a File object -func LoadFile(path string) (*File, error) { - r := new(File) - b, err := os.ReadFile(path) - if err != nil { - return r, errors.Wrapf(err, "couldn't load repositories file (%s)", path) - } - - err = yaml.Unmarshal(b, r) - return r, err -} - -// Add adds one or more repo entries to a repo file. -func (r *File) Add(re ...*Entry) { - r.Repositories = append(r.Repositories, re...) -} - -// Update attempts to replace one or more repo entries in a repo file. If an -// entry with the same name doesn't exist in the repo file it will add it. -func (r *File) Update(re ...*Entry) { - for _, target := range re { - r.update(target) - } -} - -func (r *File) update(e *Entry) { - for j, repo := range r.Repositories { - if repo.Name == e.Name { - r.Repositories[j] = e - return - } - } - r.Add(e) -} - -// Has returns true if the given name is already a repository name. -func (r *File) Has(name string) bool { - entry := r.Get(name) - return entry != nil -} - -// Get returns an entry with the given name if it exists, otherwise returns nil -func (r *File) Get(name string) *Entry { - for _, entry := range r.Repositories { - if entry.Name == name { - return entry - } - } - return nil -} - -// Remove removes the entry from the list of repositories. -func (r *File) Remove(name string) bool { - cp := []*Entry{} - found := false - for _, rf := range r.Repositories { - if rf == nil { - continue - } - if rf.Name == name { - found = true - continue - } - cp = append(cp, rf) - } - r.Repositories = cp - return found -} - -// WriteFile writes a repositories file to the given path. -func (r *File) WriteFile(path string, perm os.FileMode) error { - data, err := yaml.Marshal(r) - if err != nil { - return err - } - if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { - return err - } - return os.WriteFile(path, data, perm) -} diff --git a/pkg/helm/pkg/repo/repo_test.go b/pkg/helm/pkg/repo/repo_test.go deleted file mode 100644 index c2087ebb..00000000 --- a/pkg/helm/pkg/repo/repo_test.go +++ /dev/null @@ -1,257 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package repo - -import ( - "os" - "strings" - "testing" -) - -const testRepositoriesFile = "testdata/repositories.yaml" - -func TestFile(t *testing.T) { - rf := NewFile() - rf.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - }, - ) - - if len(rf.Repositories) != 2 { - t.Fatal("Expected 2 repositories") - } - - if rf.Has("nosuchrepo") { - t.Error("Found nonexistent repo") - } - if !rf.Has("incubator") { - t.Error("incubator repo is missing") - } - - stable := rf.Repositories[0] - if stable.Name != "stable" { - t.Error("stable is not named stable") - } - if stable.URL != "https://example.com/stable/charts" { - t.Error("Wrong URL for stable") - } -} - -func TestNewFile(t *testing.T) { - expects := NewFile() - expects.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - }, - ) - - file, err := LoadFile(testRepositoriesFile) - if err != nil { - t.Errorf("%q could not be loaded: %s", testRepositoriesFile, err) - } - - if len(expects.Repositories) != len(file.Repositories) { - t.Fatalf("Unexpected repo data: %#v", file.Repositories) - } - - for i, expect := range expects.Repositories { - got := file.Repositories[i] - if expect.Name != got.Name { - t.Errorf("Expected name %q, got %q", expect.Name, got.Name) - } - if expect.URL != got.URL { - t.Errorf("Expected url %q, got %q", expect.URL, got.URL) - } - } -} - -func TestRepoFile_Get(t *testing.T) { - repo := NewFile() - repo.Add( - &Entry{ - Name: "first", - URL: "https://example.com/first", - }, - &Entry{ - Name: "second", - URL: "https://example.com/second", - }, - &Entry{ - Name: "third", - URL: "https://example.com/third", - }, - &Entry{ - Name: "fourth", - URL: "https://example.com/fourth", - }, - ) - - name := "second" - - entry := repo.Get(name) - if entry == nil { //nolint:staticcheck - t.Fatalf("Expected repo entry %q to be found", name) - } - - if entry.URL != "https://example.com/second" { //nolint:staticcheck - t.Errorf("Expected repo URL to be %q but got %q", "https://example.com/second", entry.URL) - } - - entry = repo.Get("nonexistent") - if entry != nil { - t.Errorf("Got unexpected entry %+v", entry) - } -} - -func TestRemoveRepository(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - }, - ) - - removeRepository := "stable" - found := sampleRepository.Remove(removeRepository) - if !found { - t.Errorf("expected repository %s not found", removeRepository) - } - - found = sampleRepository.Has(removeRepository) - if found { - t.Errorf("repository %s not deleted", removeRepository) - } -} - -func TestUpdateRepository(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - }, - ) - newRepoName := "sample" - sampleRepository.Update(&Entry{Name: newRepoName, - URL: "https://example.com/sample", - }) - - if !sampleRepository.Has(newRepoName) { - t.Errorf("expected repository %s not found", newRepoName) - } - repoCount := len(sampleRepository.Repositories) - - sampleRepository.Update(&Entry{Name: newRepoName, - URL: "https://example.com/sample", - }) - - if repoCount != len(sampleRepository.Repositories) { - t.Errorf("invalid number of repositories found %d, expected number of repositories %d", len(sampleRepository.Repositories), repoCount) - } -} - -func TestWriteFile(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - }, - ) - - file, err := os.CreateTemp("", "helm-repo") - if err != nil { - t.Errorf("failed to create test-file (%v)", err) - } - defer os.Remove(file.Name()) - if err := sampleRepository.WriteFile(file.Name(), 0600); err != nil { - t.Errorf("failed to write file (%v)", err) - } - - repos, err := LoadFile(file.Name()) - if err != nil { - t.Errorf("failed to load file (%v)", err) - } - for _, repo := range sampleRepository.Repositories { - if !repos.Has(repo.Name) { - t.Errorf("expected repository %s not found", repo.Name) - } - } -} - -func TestRepoNotExists(t *testing.T) { - if _, err := LoadFile("/this/path/does/not/exist.yaml"); err == nil { - t.Errorf("expected err to be non-nil when path does not exist") - } else if !strings.Contains(err.Error(), "couldn't load repositories file") { - t.Errorf("expected prompt `couldn't load repositories file`") - } -} - -func TestRemoveRepositoryInvalidEntries(t *testing.T) { - sampleRepository := NewFile() - sampleRepository.Add( - &Entry{ - Name: "stable", - URL: "https://example.com/stable/charts", - }, - &Entry{ - Name: "incubator", - URL: "https://example.com/incubator", - }, - &Entry{}, - nil, - &Entry{ - Name: "test", - URL: "https://example.com/test", - }, - ) - - removeRepository := "stable" - found := sampleRepository.Remove(removeRepository) - if !found { - t.Errorf("expected repository %s not found", removeRepository) - } - - found = sampleRepository.Has(removeRepository) - if found { - t.Errorf("repository %s not deleted", removeRepository) - } -} diff --git a/pkg/helm/pkg/repo/repotest/server.go b/pkg/helm/pkg/repo/repotest/server.go deleted file mode 100644 index aa68da83..00000000 --- a/pkg/helm/pkg/repo/repotest/server.go +++ /dev/null @@ -1,426 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package repotest - -import ( - "context" - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "testing" - "time" - - "github.com/distribution/distribution/v3/configuration" - "github.com/distribution/distribution/v3/registry" - _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry - _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry - "github.com/phayes/freeport" - "golang.org/x/crypto/bcrypt" - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/intern/tlsutil" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - ociRegistry "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -// NewTempServerWithCleanup creates a server inside of a temp dir. -// -// If the passed in string is not "", it will be treated as a shell glob, and files -// will be copied from that path to the server's docroot. -// -// The caller is responsible for stopping the server. -// The temp dir will be removed by testing package automatically when test finished. -func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) { - srv, err := NewTempServer(glob) - t.Cleanup(func() { os.RemoveAll(srv.docroot) }) - return srv, err -} - -// Set up a fake repo with basic auth enabled -func NewTempServerWithCleanupAndBasicAuth(t *testing.T, glob string) *Server { - srv, err := NewTempServerWithCleanup(t, glob) - srv.Stop() - if err != nil { - t.Fatal(err) - } - srv.WithMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - if !ok || username != "username" || password != "password" { - t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) - } - })) - srv.Start() - return srv -} - -type OCIServer struct { - *registry.Registry - RegistryURL string - Dir string - TestUsername string - TestPassword string - Client *ociRegistry.Client -} - -type OCIServerRunConfig struct { - DependingChart *chart.Chart -} - -type OCIServerOpt func(config *OCIServerRunConfig) - -func WithDependingChart(c *chart.Chart) OCIServerOpt { - return func(config *OCIServerRunConfig) { - config.DependingChart = c - } -} - -func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { - testHtpasswdFileBasename := "authtest.htpasswd" - testUsername, testPassword := "username", "password" - - pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) - if err != nil { - t.Fatal("error generating bcrypt password for test htpasswd file") - } - htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename) - err = os.WriteFile(htpasswdPath, []byte(fmt.Sprintf("%s:%s\n", testUsername, string(pwBytes))), 0644) - if err != nil { - t.Fatalf("error creating test htpasswd file") - } - - // Registry config - config := &configuration.Configuration{} - port, err := freeport.GetFreePort() - if err != nil { - t.Fatalf("error finding free port for test registry") - } - - config.HTTP.Addr = fmt.Sprintf(":%d", port) - config.HTTP.DrainTimeout = time.Duration(10) * time.Second - config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} - config.Auth = configuration.Auth{ - "htpasswd": configuration.Parameters{ - "realm": "localhost", - "path": htpasswdPath, - }, - } - - registryURL := fmt.Sprintf("localhost:%d", port) - - r, err := registry.NewRegistry(context.Background(), config) - if err != nil { - t.Fatal(err) - } - - return &OCIServer{ - Registry: r, - RegistryURL: registryURL, - TestUsername: testUsername, - TestPassword: testPassword, - Dir: dir, - }, nil -} - -func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { - cfg := &OCIServerRunConfig{} - for _, fn := range opts { - fn(cfg) - } - - go srv.ListenAndServe() - - credentialsFile := filepath.Join(srv.Dir, "config.json") - - // init test client - registryClient, err := ociRegistry.NewClient( - ociRegistry.ClientOptDebug(true), - ociRegistry.ClientOptEnableCache(true), - ociRegistry.ClientOptWriter(os.Stdout), - ociRegistry.ClientOptCredentialsFile(credentialsFile), - ) - if err != nil { - t.Fatalf("error creating registry client") - } - - err = registryClient.Login( - srv.RegistryURL, - ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword), - ociRegistry.LoginOptInsecure(false)) - if err != nil { - t.Fatalf("error logging into registry with good credentials") - } - - ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL) - - err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz")) - if err != nil { - t.Fatal(err) - } - - // valid chart - ch, err := loader.LoadDir(filepath.Join(srv.Dir, "oci-dependent-chart")) - if err != nil { - t.Fatal("error loading chart") - } - - err = os.RemoveAll(filepath.Join(srv.Dir, "oci-dependent-chart")) - if err != nil { - t.Fatal("error removing chart before push") - } - - // save it back to disk.. - absPath, err := chartutil.Save(ch, srv.Dir) - if err != nil { - t.Fatal("could not create chart archive") - } - - // load it into memory... - contentBytes, err := os.ReadFile(absPath) - if err != nil { - t.Fatal("could not load chart into memory") - } - - result, err := registryClient.Push(contentBytes, ref, helmopts.HelmOptions{}) - if err != nil { - t.Fatalf("error pushing dependent chart: %s", err) - } - t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ - "Config.Digest: %s, Config.Size: %d, "+ - "Chart.Digest: %s, Chart.Size: %d", - result.Manifest.Digest, result.Manifest.Size, - result.Config.Digest, result.Config.Size, - result.Chart.Digest, result.Chart.Size) - - srv.Client = registryClient - c := cfg.DependingChart - if c == nil { - return - } - - dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s", - srv.RegistryURL, c.Metadata.Name, c.Metadata.Version) - - // load it into memory... - absPath = filepath.Join(srv.Dir, - fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version)) - contentBytes, err = os.ReadFile(absPath) - if err != nil { - t.Fatal("could not load chart into memory") - } - - result, err = registryClient.Push(contentBytes, dependingRef, helmopts.HelmOptions{}) - if err != nil { - t.Fatalf("error pushing depending chart: %s", err) - } - t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ - "Config.Digest: %s, Config.Size: %d, "+ - "Chart.Digest: %s, Chart.Size: %d", - result.Manifest.Digest, result.Manifest.Size, - result.Config.Digest, result.Config.Size, - result.Chart.Digest, result.Chart.Size) -} - -// NewTempServer creates a server inside of a temp dir. -// -// If the passed in string is not "", it will be treated as a shell glob, and files -// will be copied from that path to the server's docroot. -// -// The caller is responsible for destroying the temp directory as well as stopping -// the server. -// -// Deprecated: use NewTempServerWithCleanup -func NewTempServer(glob string) (*Server, error) { - tdir, err := os.MkdirTemp("", "helm-repotest-") - if err != nil { - return nil, err - } - srv := NewServer(tdir) - - if glob != "" { - if _, err := srv.CopyCharts(glob); err != nil { - srv.Stop() - return srv, err - } - } - - return srv, nil -} - -// NewServer creates a repository server for testing. -// -// docroot should be a temp dir managed by the caller. -// -// This will start the server, serving files off of the docroot. -// -// Use CopyCharts to move charts into the repository and then index them -// for service. -func NewServer(docroot string) *Server { - root, err := filepath.Abs(docroot) - if err != nil { - panic(err) - } - srv := &Server{ - docroot: root, - } - srv.Start() - // Add the testing repository as the only repo. - if err := setTestingRepository(srv.URL(), filepath.Join(root, "repositories.yaml")); err != nil { - panic(err) - } - return srv -} - -// Server is an implementation of a repository server for testing. -type Server struct { - docroot string - srv *httptest.Server - middleware http.HandlerFunc -} - -// WithMiddleware injects middleware in front of the server. This can be used to inject -// additional functionality like layering in an authentication frontend. -func (s *Server) WithMiddleware(middleware http.HandlerFunc) { - s.middleware = middleware -} - -// Root gets the docroot for the server. -func (s *Server) Root() string { - return s.docroot -} - -// CopyCharts takes a glob expression and copies those charts to the server root. -func (s *Server) CopyCharts(origin string) ([]string, error) { - files, err := filepath.Glob(origin) - if err != nil { - return []string{}, err - } - copied := make([]string, len(files)) - for i, f := range files { - base := filepath.Base(f) - newname := filepath.Join(s.docroot, base) - data, err := os.ReadFile(f) - if err != nil { - return []string{}, err - } - if err := os.WriteFile(newname, data, 0644); err != nil { - return []string{}, err - } - copied[i] = newname - } - - err = s.CreateIndex() - return copied, err -} - -// CreateIndex will read docroot and generate an index.yaml file. -func (s *Server) CreateIndex() error { - // generate the index - index, err := repo.IndexDirectory(s.docroot, s.URL(), helmopts.HelmOptions{}) - if err != nil { - return err - } - - d, err := yaml.Marshal(index) - if err != nil { - return err - } - - ifile := filepath.Join(s.docroot, "index.yaml") - return os.WriteFile(ifile, d, 0644) -} - -func (s *Server) Start() { - s.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if s.middleware != nil { - s.middleware.ServeHTTP(w, r) - } - http.FileServer(http.Dir(s.docroot)).ServeHTTP(w, r) - })) -} - -func (s *Server) StartTLS() { - cd := "../../testdata" - ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") - insecure := false - - s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if s.middleware != nil { - s.middleware.ServeHTTP(w, r) - } - http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r) - })) - tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca, insecure) - if err != nil { - panic(err) - } - tlsConf.ServerName = "helm.sh" - s.srv.TLS = tlsConf - s.srv.StartTLS() - - // Set up repositories config with ca file - repoConfig := filepath.Join(s.Root(), "repositories.yaml") - - r := repo.NewFile() - r.Add(&repo.Entry{ - Name: "test", - URL: s.URL(), - CAFile: filepath.Join("../../testdata", "rootca.crt"), - }) - - if err := r.WriteFile(repoConfig, 0600); err != nil { - panic(err) - } -} - -// Stop stops the server and closes all connections. -// -// It should be called explicitly. -func (s *Server) Stop() { - s.srv.Close() -} - -// URL returns the URL of the server. -// -// Example: -// -// http://localhost:1776 -func (s *Server) URL() string { - return s.srv.URL -} - -// LinkIndices links the index created with CreateIndex and makes a symbolic link to the cache index. -// -// This makes it possible to simulate a local cache of a repository. -func (s *Server) LinkIndices() error { - lstart := filepath.Join(s.docroot, "index.yaml") - ldest := filepath.Join(s.docroot, "test-index.yaml") - return os.Symlink(lstart, ldest) -} - -// setTestingRepository sets up a testing repository.yaml with only the given URL. -func setTestingRepository(url, fname string) error { - r := repo.NewFile() - r.Add(&repo.Entry{ - Name: "test", - URL: url, - }) - return r.WriteFile(fname, 0640) -} diff --git a/pkg/helm/pkg/repo/repotest/server_test.go b/pkg/helm/pkg/repo/repotest/server_test.go deleted file mode 100644 index adf5b63a..00000000 --- a/pkg/helm/pkg/repo/repotest/server_test.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright The Helm Authors. -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package repotest - -import ( - "io" - "net/http" - "path/filepath" - "testing" - - "sigs.k8s.io/yaml" - - "github.com/werf/nelm/pkg/helm/intern/test/ensure" - "github.com/werf/nelm/pkg/helm/pkg/repo" -) - -// Young'n, in these here parts, we test our tests. - -func TestServer(t *testing.T) { - ensure.HelmHome(t) - - rootDir := t.TempDir() - - srv := NewServer(rootDir) - defer srv.Stop() - - c, err := srv.CopyCharts("testdata/*.tgz") - if err != nil { - // Some versions of Go don't correctly fire defer on Fatal. - t.Fatal(err) - } - - if len(c) != 1 { - t.Errorf("Unexpected chart count: %d", len(c)) - } - - if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" { - t.Errorf("Unexpected chart: %s", c[0]) - } - - res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") - res.Body.Close() - if err != nil { - t.Fatal(err) - } - - if res.ContentLength < 500 { - t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) - } - - res, err = http.Get(srv.URL() + "/index.yaml") - if err != nil { - t.Fatal(err) - } - - data, err := io.ReadAll(res.Body) - res.Body.Close() - if err != nil { - t.Fatal(err) - } - - m := repo.NewIndexFile() - if err := yaml.Unmarshal(data, m); err != nil { - t.Fatal(err) - } - - if l := len(m.Entries); l != 1 { - t.Fatalf("Expected 1 entry, got %d", l) - } - - expect := "examplechart" - if !m.Has(expect, "0.1.0") { - t.Errorf("missing %q", expect) - } - - res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") - res.Body.Close() - if err != nil { - t.Fatal(err) - } - if res.StatusCode != 404 { - t.Fatalf("Expected 404, got %d", res.StatusCode) - } -} - -func TestNewTempServer(t *testing.T) { - ensure.HelmHome(t) - - srv, err := NewTempServerWithCleanup(t, "testdata/examplechart-0.1.0.tgz") - if err != nil { - t.Fatal(err) - } - defer srv.Stop() - - res, err := http.Head(srv.URL() + "/examplechart-0.1.0.tgz") - res.Body.Close() - if err != nil { - t.Error(err) - } - if res.StatusCode != 200 { - t.Errorf("Expected 200, got %d", res.StatusCode) - } -} diff --git a/pkg/helm/pkg/repo/v1/chartrepo.go b/pkg/helm/pkg/repo/v1/chartrepo.go new file mode 100644 index 00000000..c1d384b0 --- /dev/null +++ b/pkg/helm/pkg/repo/v1/chartrepo.go @@ -0,0 +1,276 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repo // import "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + +import ( + "bytes" + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/werf/nelm/pkg/helm/intern/fileutil" + "github.com/werf/nelm/pkg/helm/pkg/getter" + "github.com/werf/nelm/pkg/helm/pkg/helmpath" +) + +// Entry represents a collection of parameters for chart repository +type Entry struct { + Name string `json:"name"` + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password"` + CertFile string `json:"certFile"` + KeyFile string `json:"keyFile"` + CAFile string `json:"caFile"` + InsecureSkipTLSVerify bool `json:"insecure_skip_tls_verify"` + PassCredentialsAll bool `json:"pass_credentials_all"` +} + +// ChartRepository represents a chart repository +type ChartRepository struct { + Config *Entry + IndexFile *IndexFile + Client getter.Getter + CachePath string +} + +// NewChartRepository constructs ChartRepository +func NewChartRepository(cfg *Entry, getters getter.Providers) (*ChartRepository, error) { + u, err := url.Parse(cfg.URL) + if err != nil { + return nil, fmt.Errorf("invalid chart URL format: %s", cfg.URL) + } + + client, err := getters.ByScheme(u.Scheme) + if err != nil { + return nil, fmt.Errorf("could not find protocol handler for: %s", u.Scheme) + } + + return &ChartRepository{ + Config: cfg, + IndexFile: NewIndexFile(), + Client: client, + CachePath: helmpath.CachePath("repository"), + }, nil +} + +// DownloadIndexFile fetches the index from a repository. +func (r *ChartRepository) DownloadIndexFile() (string, error) { + indexURL, err := ResolveReferenceURL(r.Config.URL, "index.yaml") + if err != nil { + return "", err + } + + resp, err := r.Client.Get(indexURL, + getter.WithURL(r.Config.URL), + getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSVerify), + getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile), + getter.WithBasicAuth(r.Config.Username, r.Config.Password), + getter.WithPassCredentialsAll(r.Config.PassCredentialsAll), + ) + if err != nil { + return "", err + } + + index, err := io.ReadAll(resp) + if err != nil { + return "", err + } + + indexFile, err := loadIndex(index, r.Config.URL) + if err != nil { + return "", err + } + + // Create the chart list file in the cache directory + var charts strings.Builder + for name := range indexFile.Entries { + fmt.Fprintln(&charts, name) + } + chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) + os.MkdirAll(filepath.Dir(chartsFile), 0755) + + fileutil.AtomicWriteFile(chartsFile, bytes.NewReader([]byte(charts.String())), 0644) + + // Create the index file in the cache directory + fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)) + os.MkdirAll(filepath.Dir(fname), 0755) + return fname, fileutil.AtomicWriteFile(fname, bytes.NewReader(index), 0644) +} + +type findChartInRepoURLOptions struct { + Username string + Password string + PassCredentialsAll bool + InsecureSkipTLSVerify bool + CertFile string + KeyFile string + CAFile string + ChartVersion string +} + +type FindChartInRepoURLOption func(*findChartInRepoURLOptions) + +// WithChartVersion specifies the chart version to find +func WithChartVersion(chartVersion string) FindChartInRepoURLOption { + return func(options *findChartInRepoURLOptions) { + options.ChartVersion = chartVersion + } +} + +// WithUsernamePassword specifies the username/password credntials for the repository +func WithUsernamePassword(username, password string) FindChartInRepoURLOption { + return func(options *findChartInRepoURLOptions) { + options.Username = username + options.Password = password + } +} + +// WithPassCredentialsAll flags whether credentials should be passed on to other domains +func WithPassCredentialsAll(passCredentialsAll bool) FindChartInRepoURLOption { + return func(options *findChartInRepoURLOptions) { + options.PassCredentialsAll = passCredentialsAll + } +} + +// WithClientTLS species the cert, key, and CA files for client mTLS +func WithClientTLS(certFile, keyFile, caFile string) FindChartInRepoURLOption { + return func(options *findChartInRepoURLOptions) { + options.CertFile = certFile + options.KeyFile = keyFile + options.CAFile = caFile + } +} + +// WithInsecureSkipTLSVerify skips TLS verification for repository communication +func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) FindChartInRepoURLOption { + return func(options *findChartInRepoURLOptions) { + options.InsecureSkipTLSVerify = insecureSkipTLSVerify + } +} + +// FindChartInRepoURL finds chart in chart repository pointed by repoURL +// without adding repo to repositories +func FindChartInRepoURL(repoURL string, chartName string, getters getter.Providers, options ...FindChartInRepoURLOption) (string, error) { + + opts := findChartInRepoURLOptions{} + for _, option := range options { + option(&opts) + } + + // Download and write the index file to a temporary location + buf := make([]byte, 20) + rand.Read(buf) + name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-") + + c := Entry{ + URL: repoURL, + Username: opts.Username, + Password: opts.Password, + PassCredentialsAll: opts.PassCredentialsAll, + CertFile: opts.CertFile, + KeyFile: opts.KeyFile, + CAFile: opts.CAFile, + Name: name, + InsecureSkipTLSVerify: opts.InsecureSkipTLSVerify, + } + r, err := NewChartRepository(&c, getters) + if err != nil { + return "", err + } + idx, err := r.DownloadIndexFile() + if err != nil { + return "", fmt.Errorf("looks like %q is not a valid chart repository or cannot be reached: %w", repoURL, err) + } + defer func() { + os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))) + os.RemoveAll(filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))) + }() + + // Read the index file for the repository to get chart information and return chart URL + repoIndex, err := LoadIndexFile(idx) + if err != nil { + return "", err + } + + errMsg := fmt.Sprintf("chart %q", chartName) + if opts.ChartVersion != "" { + errMsg = fmt.Sprintf("%s version %q", errMsg, opts.ChartVersion) + } + cv, err := repoIndex.Get(chartName, opts.ChartVersion) + if err != nil { + return "", ChartNotFoundError{ + Chart: errMsg, + RepoURL: repoURL, + } + } + + if len(cv.URLs) == 0 { + return "", fmt.Errorf("%s has no downloadable URLs", errMsg) + } + + chartURL := cv.URLs[0] + + absoluteChartURL, err := ResolveReferenceURL(repoURL, chartURL) + if err != nil { + return "", fmt.Errorf("failed to make chart URL absolute: %w", err) + } + + return absoluteChartURL, nil +} + +// ResolveReferenceURL resolves refURL relative to baseURL. +// If refURL is absolute, it simply returns refURL. +func ResolveReferenceURL(baseURL, refURL string) (string, error) { + parsedRefURL, err := url.Parse(refURL) + if err != nil { + return "", fmt.Errorf("failed to parse %s as URL: %w", refURL, err) + } + + if parsedRefURL.IsAbs() { + return refURL, nil + } + + parsedBaseURL, err := url.Parse(baseURL) + if err != nil { + return "", fmt.Errorf("failed to parse %s as URL: %w", baseURL, err) + } + + // We need a trailing slash for ResolveReference to work, but make sure there isn't already one + parsedBaseURL.RawPath = strings.TrimSuffix(parsedBaseURL.RawPath, "/") + "/" + parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/" + + resolvedURL := parsedBaseURL.ResolveReference(parsedRefURL) + resolvedURL.RawQuery = parsedBaseURL.RawQuery + return resolvedURL.String(), nil +} + +func (e *Entry) String() string { + buf, err := json.Marshal(e) + if err != nil { + slog.Error("failed to marshal entry", slog.Any("error", err)) + panic(err) + } + return string(buf) +} diff --git a/pkg/helm/pkg/repo/v1/chartrepo_test.go b/pkg/helm/pkg/repo/v1/chartrepo_test.go new file mode 100644 index 00000000..4b06bfa1 --- /dev/null +++ b/pkg/helm/pkg/repo/v1/chartrepo_test.go @@ -0,0 +1,302 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repo + +import ( + "bytes" + "errors" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "testing" + "time" + + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/pkg/cli" + "github.com/werf/nelm/pkg/helm/pkg/getter" + "github.com/werf/nelm/pkg/helm/pkg/helmpath" +) + +type CustomGetter struct { + repoUrls []string +} + +func (g *CustomGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) { + index := &IndexFile{ + APIVersion: "v1", + Generated: time.Now(), + } + indexBytes, err := yaml.Marshal(index) + if err != nil { + return nil, err + } + g.repoUrls = append(g.repoUrls, href) + return bytes.NewBuffer(indexBytes), nil +} + +func TestIndexCustomSchemeDownload(t *testing.T) { + repoName := "gcs-repo" + repoURL := "gs://some-gcs-bucket" + myCustomGetter := &CustomGetter{} + customGetterConstructor := func(_ ...getter.Option) (getter.Getter, error) { + return myCustomGetter, nil + } + providers := getter.Providers{{ + Schemes: []string{"gs"}, + New: customGetterConstructor, + }} + repo, err := NewChartRepository(&Entry{ + Name: repoName, + URL: repoURL, + }, providers) + if err != nil { + t.Fatalf("Problem loading chart repository from %s: %v", repoURL, err) + } + repo.CachePath = t.TempDir() + + tempIndexFile, err := os.CreateTemp(t.TempDir(), "test-repo") + if err != nil { + t.Fatalf("Failed to create temp index file: %v", err) + } + defer os.Remove(tempIndexFile.Name()) + + idx, err := repo.DownloadIndexFile() + if err != nil { + t.Fatalf("Failed to download index file to %s: %v", idx, err) + } + + if len(myCustomGetter.repoUrls) != 1 { + t.Fatalf("Custom Getter.Get should be called once") + } + + expectedRepoIndexURL := repoURL + "/index.yaml" + if myCustomGetter.repoUrls[0] != expectedRepoIndexURL { + t.Fatalf("Custom Getter.Get should be called with %s", expectedRepoIndexURL) + } +} + +func TestConcurrencyDownloadIndex(t *testing.T) { + srv, err := startLocalServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + repo, err := NewChartRepository(&Entry{ + Name: "nginx", + URL: srv.URL, + }, getter.All(&cli.EnvSettings{})) + + if err != nil { + t.Fatalf("Problem loading chart repository from %s: %v", srv.URL, err) + } + repo.CachePath = t.TempDir() + + // initial download index + idx, err := repo.DownloadIndexFile() + if err != nil { + t.Fatalf("Failed to download index file to %s: %v", idx, err) + } + + indexFName := filepath.Join(repo.CachePath, helmpath.CacheIndexFile(repo.Config.Name)) + + var wg sync.WaitGroup + + // Simultaneously start multiple goroutines that: + // 1) download index.yaml via DownloadIndexFile (write operation), + // 2) read index.yaml via LoadIndexFile (read operation). + // This checks for race conditions and ensures correct behavior under concurrent read/write access. + for range 150 { + wg.Add(1) + + go func() { + defer wg.Done() + idx, err := repo.DownloadIndexFile() + if err != nil { + t.Errorf("Failed to download index file to %s: %v", idx, err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + _, err := LoadIndexFile(indexFName) + if err != nil { + t.Errorf("Failed to load index file: %v", err) + } + }() + } + wg.Wait() +} + +// startLocalServerForTests Start the local helm server +func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) { + if handler == nil { + fileBytes, err := os.ReadFile("testdata/local-index.yaml") + if err != nil { + return nil, err + } + handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(fileBytes) + }) + } + + return httptest.NewServer(handler), nil +} + +// startLocalTLSServerForTests Start the local helm server with TLS +func startLocalTLSServerForTests(handler http.Handler) (*httptest.Server, error) { + if handler == nil { + fileBytes, err := os.ReadFile("testdata/local-index.yaml") + if err != nil { + return nil, err + } + handler = http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write(fileBytes) + }) + } + + return httptest.NewTLSServer(handler), nil +} + +func TestFindChartInAuthAndTLSAndPassRepoURL(t *testing.T) { + srv, err := startLocalTLSServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + chartURL, err := FindChartInRepoURL( + srv.URL, + "nginx", + getter.All(&cli.EnvSettings{}), + WithInsecureSkipTLSVerify(true), + ) + if err != nil { + t.Fatalf("%v", err) + } + if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { + t.Errorf("%s is not the valid URL", chartURL) + } + + // If the insecureSkipTLSVerify is false, it will return an error that contains "x509: certificate signed by unknown authority". + _, err = FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{}), WithChartVersion("0.1.0")) + // Go communicates with the platform and different platforms return different messages. Go itself tests darwin + // differently for its message. On newer versions of Darwin the message includes the "Acme Co" portion while older + // versions of Darwin do not. As there are people developing Helm using both old and new versions of Darwin we test + // for both messages. + if runtime.GOOS == "darwin" { + if !strings.Contains(err.Error(), "x509: “Acme Co” certificate is not trusted") && !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { + t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) + } + } else if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { + t.Errorf("Expected TLS error for function FindChartInAuthAndTLSAndPassRepoURL not found, but got a different error (%v)", err) + } +} + +func TestFindChartInRepoURL(t *testing.T) { + srv, err := startLocalServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + chartURL, err := FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{})) + if err != nil { + t.Fatalf("%v", err) + } + if chartURL != "https://charts.helm.sh/stable/nginx-0.2.0.tgz" { + t.Errorf("%s is not the valid URL", chartURL) + } + + chartURL, err = FindChartInRepoURL(srv.URL, "nginx", getter.All(&cli.EnvSettings{}), WithChartVersion("0.1.0")) + if err != nil { + t.Errorf("%s", err) + } + if chartURL != "https://charts.helm.sh/stable/nginx-0.1.0.tgz" { + t.Errorf("%s is not the valid URL", chartURL) + } +} + +func TestErrorFindChartInRepoURL(t *testing.T) { + + g := getter.All(&cli.EnvSettings{ + RepositoryCache: t.TempDir(), + }) + + if _, err := FindChartInRepoURL("http://someserver/something", "nginx", g); err == nil { + t.Errorf("Expected error for bad chart URL, but did not get any errors") + } else if !strings.Contains(err.Error(), `looks like "http://someserver/something" is not a valid chart repository or cannot be reached`) { + t.Errorf("Expected error for bad chart URL, but got a different error (%v)", err) + } + + srv, err := startLocalServerForTests(nil) + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + if _, err = FindChartInRepoURL(srv.URL, "nginx1", g); err == nil { + t.Errorf("Expected error for chart not found, but did not get any errors") + } else if err.Error() != `chart "nginx1" not found in `+srv.URL+` repository` { + t.Errorf("Expected error for chart not found, but got a different error (%v)", err) + } + if !errors.Is(err, ChartNotFoundError{}) { + t.Errorf("error is not of correct error type structure") + } + + if _, err = FindChartInRepoURL(srv.URL, "nginx1", g, WithChartVersion("0.1.0")); err == nil { + t.Errorf("Expected error for chart not found, but did not get any errors") + } else if err.Error() != `chart "nginx1" version "0.1.0" not found in `+srv.URL+` repository` { + t.Errorf("Expected error for chart not found, but got a different error (%v)", err) + } + + if _, err = FindChartInRepoURL(srv.URL, "chartWithNoURL", g); err == nil { + t.Errorf("Expected error for no chart URLs available, but did not get any errors") + } else if err.Error() != `chart "chartWithNoURL" has no downloadable URLs` { + t.Errorf("Expected error for chart not found, but got a different error (%v)", err) + } +} + +func TestResolveReferenceURL(t *testing.T) { + for _, tt := range []struct { + baseURL, refURL, chartURL string + }{ + {"http://localhost:8123/", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts/", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts/", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts-with-no-trailing-slash", "nginx-0.2.0.tgz", "http://localhost:8123/charts-with-no-trailing-slash/nginx-0.2.0.tgz"}, + {"http://localhost:8123", "https://charts.helm.sh/stable/nginx-0.2.0.tgz", "https://charts.helm.sh/stable/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "nginx-0.2.0.tgz", "http://localhost:8123/charts%2fwith%2fescaped%2fslash/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts%2fwith%2fescaped%2fslash", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz"}, + {"http://localhost:8123/charts?with=queryparameter", "nginx-0.2.0.tgz", "http://localhost:8123/charts/nginx-0.2.0.tgz?with=queryparameter"}, + {"http://localhost:8123/charts?with=queryparameter", "/nginx-0.2.0.tgz", "http://localhost:8123/nginx-0.2.0.tgz?with=queryparameter"}, + } { + chartURL, err := ResolveReferenceURL(tt.baseURL, tt.refURL) + if err != nil { + t.Errorf("unexpected error in ResolveReferenceURL(%q, %q): %s", tt.baseURL, tt.refURL, err) + } + if chartURL != tt.chartURL { + t.Errorf("expected ResolveReferenceURL(%q, %q) to equal %q, got %q", tt.baseURL, tt.refURL, tt.chartURL, chartURL) + } + } +} diff --git a/pkg/helm/pkg/repo/doc.go b/pkg/helm/pkg/repo/v1/doc.go similarity index 100% rename from pkg/helm/pkg/repo/doc.go rename to pkg/helm/pkg/repo/v1/doc.go diff --git a/pkg/helm/pkg/repo/v1/error.go b/pkg/helm/pkg/repo/v1/error.go new file mode 100644 index 00000000..16264ed2 --- /dev/null +++ b/pkg/helm/pkg/repo/v1/error.go @@ -0,0 +1,35 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repo + +import ( + "fmt" +) + +type ChartNotFoundError struct { + RepoURL string + Chart string +} + +func (e ChartNotFoundError) Error() string { + return fmt.Sprintf("%s not found in %s repository", e.Chart, e.RepoURL) +} + +func (e ChartNotFoundError) Is(err error) bool { + _, ok := err.(ChartNotFoundError) + return ok +} diff --git a/pkg/helm/pkg/repo/index.go b/pkg/helm/pkg/repo/v1/index.go similarity index 89% rename from pkg/helm/pkg/repo/index.go rename to pkg/helm/pkg/repo/v1/index.go index 9e77ae4a..ce2d354b 100644 --- a/pkg/helm/pkg/repo/index.go +++ b/pkg/helm/pkg/repo/v1/index.go @@ -18,8 +18,11 @@ package repo import ( "bytes" + "context" "encoding/json" - "log" + "errors" + "fmt" + "log/slog" "os" "path" "path/filepath" @@ -28,19 +31,15 @@ import ( "time" "github.com/Masterminds/semver/v3" - "github.com/pkg/errors" "sigs.k8s.io/yaml" "github.com/werf/nelm/pkg/helm/intern/fileutil" "github.com/werf/nelm/pkg/helm/intern/urlutil" - "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chart/loader" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" "github.com/werf/nelm/pkg/helm/pkg/provenance" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) -var indexPath = "index.yaml" - // APIVersionV1 is the v1 API version for index and repository files. const APIVersionV1 = "v1" @@ -111,7 +110,7 @@ func LoadIndexFile(path string) (*IndexFile, error) { } i, err := loadIndex(b, path) if err != nil { - return nil, errors.Wrapf(err, "error loading %s", path) + return nil, fmt.Errorf("error loading %s: %w", path, err) } return i, nil } @@ -127,7 +126,7 @@ func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) md.APIVersion = chart.APIVersionV1 } if err := md.Validate(); err != nil { - return errors.Wrapf(err, "validate failed for %s", filename) + return fmt.Errorf("validate failed for %s: %w", filename, err) } u := filename @@ -155,7 +154,7 @@ func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string) // Deprecated: Use index.MustAdd instead. func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { if err := i.MustAdd(md, filename, baseURL, digest); err != nil { - log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err) + slog.Error("skipping loading invalid entry for chart %q %q from %s: %s", md.Name, md.Version, filename, err) } } @@ -201,7 +200,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { } } - // when customer input exact version, check whether have exact match one first + // when customer inputs specific version, check whether there's an exact match first if len(version) != 0 { for _, ver := range vs { if version == ver.Version { @@ -217,10 +216,13 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { } if constraint.Check(test) { + if len(version) != 0 { + slog.Warn("unable to find exact version requested; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version) + } return ver, nil } } - return nil, errors.Errorf("no chart version found for %s-%s", name, version) + return nil, fmt.Errorf("no chart version found for %s-%s", name, version) } // WriteFile writes an index file to the given destination path. @@ -296,7 +298,7 @@ type ChartVersion struct { // It indexes only charts that have been packaged (*.tgz). // // The index returned will be in an unsorted state -func IndexDirectory(dir, baseURL string, opts helmopts.HelmOptions) (*IndexFile, error) { +func IndexDirectory(dir, baseURL string) (*IndexFile, error) { archives, err := filepath.Glob(filepath.Join(dir, "*.tgz")) if err != nil { return nil, err @@ -323,7 +325,7 @@ func IndexDirectory(dir, baseURL string, opts helmopts.HelmOptions) (*IndexFile, parentURL = path.Join(baseURL, parentDir) } - c, err := loader.Load(arch, opts) + c, err := loader.Load(context.Background(), arch) if err != nil { // Assume this is not a chart. continue @@ -333,7 +335,7 @@ func IndexDirectory(dir, baseURL string, opts helmopts.HelmOptions) (*IndexFile, return index, err } if err := index.MustAdd(c.Metadata, fname, parentURL, hash); err != nil { - return index, errors.Wrapf(err, "failed adding to %s to index", fname) + return index, fmt.Errorf("failed adding to %s to index: %w", fname, err) } } return index, nil @@ -357,7 +359,8 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { for name, cvs := range i.Entries { for idx := len(cvs) - 1; idx >= 0; idx-- { if cvs[idx] == nil { - log.Printf("skipping loading invalid entry for chart %q from %s: empty entry", name, source) + slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q from %s: empty entry", name, source)) + cvs = append(cvs[:idx], cvs[idx+1:]...) continue } // When metadata section missing, initialize with no data @@ -368,10 +371,12 @@ func loadIndex(data []byte, source string) (*IndexFile, error) { cvs[idx].APIVersion = chart.APIVersionV1 } if err := cvs[idx].Validate(); ignoreSkippableChartValidationError(err) != nil { - log.Printf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err) + slog.Warn(fmt.Sprintf("skipping loading invalid entry for chart %q %q from %s: %s", name, cvs[idx].Version, source, err)) cvs = append(cvs[:idx], cvs[idx+1:]...) } } + // adjust slice to only contain a set of valid versions + i.Entries[name] = cvs } i.SortEntries() if i.APIVersion == "" { @@ -398,7 +403,7 @@ func jsonOrYamlUnmarshal(b []byte, i interface{}) error { // the error isn't important for index loading // // In particular, charts may introduce validations that don't impact repository indexes -// And repository indexes may be generated by older/non-complient software, which doesn't +// And repository indexes may be generated by older/non-compliant software, which doesn't // conform to all validations. func ignoreSkippableChartValidationError(err error) error { verr, ok := err.(chart.ValidationError) diff --git a/pkg/helm/pkg/repo/index_test.go b/pkg/helm/pkg/repo/v1/index_test.go similarity index 89% rename from pkg/helm/pkg/repo/index_test.go rename to pkg/helm/pkg/repo/v1/index_test.go index 97f0bc4f..9dedb23b 100644 --- a/pkg/helm/pkg/repo/index_test.go +++ b/pkg/helm/pkg/repo/v1/index_test.go @@ -28,7 +28,7 @@ import ( "strings" "testing" - "github.com/werf/nelm/pkg/helm/pkg/chart" + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" "github.com/werf/nelm/pkg/helm/pkg/cli" "github.com/werf/nelm/pkg/helm/pkg/getter" "github.com/werf/nelm/pkg/helm/pkg/helmpath" @@ -68,6 +68,7 @@ entries: grafana: - apiVersion: v2 name: grafana + - null foo: - bar: @@ -123,17 +124,17 @@ func TestIndexFile(t *testing.T) { } cv, err := i.Get("setter", "0.1.9") - if err == nil && !strings.Contains(cv.Metadata.Version, "0.1.9") { - t.Errorf("Unexpected version: %s", cv.Metadata.Version) + if err == nil && !strings.Contains(cv.Version, "0.1.9") { + t.Errorf("Unexpected version: %s", cv.Version) } cv, err = i.Get("setter", "0.1.9+alpha") - if err != nil || cv.Metadata.Version != "0.1.9+alpha" { + if err != nil || cv.Version != "0.1.9+alpha" { t.Errorf("Expected version: 0.1.9+alpha") } cv, err = i.Get("setter", "0.1.8") - if err != nil || cv.Metadata.Version != "0.1.8" { + if err != nil || cv.Version != "0.1.8" { t.Errorf("Expected version: 0.1.8") } } @@ -159,7 +160,6 @@ func TestLoadIndex(t *testing.T) { } for _, tc := range tests { - tc := tc t.Run(tc.Name, func(t *testing.T) { t.Parallel() i, err := LoadIndexFile(tc.Filename) @@ -352,6 +352,7 @@ func TestDownloadIndexFile(t *testing.T) { } func verifyLocalIndex(t *testing.T, i *IndexFile) { + t.Helper() numEntries := len(i.Entries) if numEntries != 3 { t.Errorf("Expected 3 entries in index file but got %d", numEntries) @@ -450,6 +451,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) { } func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *IndexFile) { + t.Helper() var expected, reald []string for chart := range indexContent.Entries { expected = append(expected, chart) @@ -644,3 +646,75 @@ func TestIgnoreSkippableChartValidationError(t *testing.T) { }) } } + +var indexWithDuplicatesInChartDeps = ` +apiVersion: v1 +entries: + nginx: + - urls: + - https://charts.helm.sh/stable/alpine-1.0.0.tgz + - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz + name: alpine + description: string + home: https://github.com/something + digest: "sha256:1234567890abcdef" + - urls: + - https://charts.helm.sh/stable/nginx-0.2.0.tgz + name: nginx + description: string + version: 0.2.0 + home: https://github.com/something/else + digest: "sha256:1234567890abcdef" +` +var indexWithDuplicatesInLastChartDeps = ` +apiVersion: v1 +entries: + nginx: + - urls: + - https://charts.helm.sh/stable/nginx-0.2.0.tgz + name: nginx + description: string + version: 0.2.0 + home: https://github.com/something/else + digest: "sha256:1234567890abcdef" + - urls: + - https://charts.helm.sh/stable/alpine-1.0.0.tgz + - http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz + name: alpine + description: string + home: https://github.com/something + digest: "sha256:111" +` + +func TestLoadIndex_DuplicateChartDeps(t *testing.T) { + tests := []struct { + source string + data string + }{ + { + source: "indexWithDuplicatesInChartDeps", + data: indexWithDuplicatesInChartDeps, + }, + { + source: "indexWithDuplicatesInLastChartDeps", + data: indexWithDuplicatesInLastChartDeps, + }, + } + for _, tc := range tests { + t.Run(tc.source, func(t *testing.T) { + idx, err := loadIndex([]byte(tc.data), tc.source) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + cvs := idx.Entries["nginx"] + if cvs == nil { + t.Error("expected one chart version not to be filtered out") + } + for _, v := range cvs { + if v.Name == "alpine" { + t.Error("malformed version was not filtered out") + } + } + }) + } +} diff --git a/pkg/helm/pkg/repo/v1/repo.go b/pkg/helm/pkg/repo/v1/repo.go new file mode 100644 index 00000000..036c1bb2 --- /dev/null +++ b/pkg/helm/pkg/repo/v1/repo.go @@ -0,0 +1,125 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repo // import "github.com/werf/nelm/pkg/helm/pkg/repo/v1" + +import ( + "fmt" + "os" + "path/filepath" + "time" + + "sigs.k8s.io/yaml" +) + +// File represents the repositories.yaml file +type File struct { + APIVersion string `json:"apiVersion"` + Generated time.Time `json:"generated"` + Repositories []*Entry `json:"repositories"` +} + +// NewFile generates an empty repositories file. +// +// Generated and APIVersion are automatically set. +func NewFile() *File { + return &File{ + APIVersion: APIVersionV1, + Generated: time.Now(), + Repositories: []*Entry{}, + } +} + +// LoadFile takes a file at the given path and returns a File object +func LoadFile(path string) (*File, error) { + r := new(File) + b, err := os.ReadFile(path) + if err != nil { + return r, fmt.Errorf("couldn't load repositories file (%s): %w", path, err) + } + + err = yaml.Unmarshal(b, r) + return r, err +} + +// Add adds one or more repo entries to a repo file. +func (r *File) Add(re ...*Entry) { + r.Repositories = append(r.Repositories, re...) +} + +// Update attempts to replace one or more repo entries in a repo file. If an +// entry with the same name doesn't exist in the repo file it will add it. +func (r *File) Update(re ...*Entry) { + for _, target := range re { + r.update(target) + } +} + +func (r *File) update(e *Entry) { + for j, repo := range r.Repositories { + if repo.Name == e.Name { + r.Repositories[j] = e + return + } + } + r.Add(e) +} + +// Has returns true if the given name is already a repository name. +func (r *File) Has(name string) bool { + entry := r.Get(name) + return entry != nil +} + +// Get returns an entry with the given name if it exists, otherwise returns nil +func (r *File) Get(name string) *Entry { + for _, entry := range r.Repositories { + if entry.Name == name { + return entry + } + } + return nil +} + +// Remove removes the entry from the list of repositories. +func (r *File) Remove(name string) bool { + cp := []*Entry{} + found := false + for _, rf := range r.Repositories { + if rf == nil { + continue + } + if rf.Name == name { + found = true + continue + } + cp = append(cp, rf) + } + r.Repositories = cp + return found +} + +// WriteFile writes a repositories file to the given path. +func (r *File) WriteFile(path string, perm os.FileMode) error { + data, err := yaml.Marshal(r) + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, data, perm) +} diff --git a/pkg/helm/pkg/repo/v1/repo_test.go b/pkg/helm/pkg/repo/v1/repo_test.go new file mode 100644 index 00000000..bdaa61ed --- /dev/null +++ b/pkg/helm/pkg/repo/v1/repo_test.go @@ -0,0 +1,257 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repo + +import ( + "os" + "strings" + "testing" +) + +const testRepositoriesFile = "testdata/repositories.yaml" + +func TestFile(t *testing.T) { + rf := NewFile() + rf.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + ) + + if len(rf.Repositories) != 2 { + t.Fatal("Expected 2 repositories") + } + + if rf.Has("nosuchrepo") { + t.Error("Found nonexistent repo") + } + if !rf.Has("incubator") { + t.Error("incubator repo is missing") + } + + stable := rf.Repositories[0] + if stable.Name != "stable" { + t.Error("stable is not named stable") + } + if stable.URL != "https://example.com/stable/charts" { + t.Error("Wrong URL for stable") + } +} + +func TestNewFile(t *testing.T) { + expects := NewFile() + expects.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + ) + + file, err := LoadFile(testRepositoriesFile) + if err != nil { + t.Errorf("%q could not be loaded: %s", testRepositoriesFile, err) + } + + if len(expects.Repositories) != len(file.Repositories) { + t.Fatalf("Unexpected repo data: %#v", file.Repositories) + } + + for i, expect := range expects.Repositories { + got := file.Repositories[i] + if expect.Name != got.Name { + t.Errorf("Expected name %q, got %q", expect.Name, got.Name) + } + if expect.URL != got.URL { + t.Errorf("Expected url %q, got %q", expect.URL, got.URL) + } + } +} + +func TestRepoFile_Get(t *testing.T) { + repo := NewFile() + repo.Add( + &Entry{ + Name: "first", + URL: "https://example.com/first", + }, + &Entry{ + Name: "second", + URL: "https://example.com/second", + }, + &Entry{ + Name: "third", + URL: "https://example.com/third", + }, + &Entry{ + Name: "fourth", + URL: "https://example.com/fourth", + }, + ) + + name := "second" + + entry := repo.Get(name) + if entry == nil { //nolint:staticcheck + t.Fatalf("Expected repo entry %q to be found", name) + } + + if entry.URL != "https://example.com/second" { //nolint:staticcheck + t.Errorf("Expected repo URL to be %q but got %q", "https://example.com/second", entry.URL) + } + + entry = repo.Get("nonexistent") + if entry != nil { + t.Errorf("Got unexpected entry %+v", entry) + } +} + +func TestRemoveRepository(t *testing.T) { + sampleRepository := NewFile() + sampleRepository.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + ) + + removeRepository := "stable" + found := sampleRepository.Remove(removeRepository) + if !found { + t.Errorf("expected repository %s not found", removeRepository) + } + + found = sampleRepository.Has(removeRepository) + if found { + t.Errorf("repository %s not deleted", removeRepository) + } +} + +func TestUpdateRepository(t *testing.T) { + sampleRepository := NewFile() + sampleRepository.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + ) + newRepoName := "sample" + sampleRepository.Update(&Entry{Name: newRepoName, + URL: "https://example.com/sample", + }) + + if !sampleRepository.Has(newRepoName) { + t.Errorf("expected repository %s not found", newRepoName) + } + repoCount := len(sampleRepository.Repositories) + + sampleRepository.Update(&Entry{Name: newRepoName, + URL: "https://example.com/sample", + }) + + if repoCount != len(sampleRepository.Repositories) { + t.Errorf("invalid number of repositories found %d, expected number of repositories %d", len(sampleRepository.Repositories), repoCount) + } +} + +func TestWriteFile(t *testing.T) { + sampleRepository := NewFile() + sampleRepository.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + ) + + file, err := os.CreateTemp(t.TempDir(), "helm-repo") + if err != nil { + t.Errorf("failed to create test-file (%v)", err) + } + defer os.Remove(file.Name()) + if err := sampleRepository.WriteFile(file.Name(), 0600); err != nil { + t.Errorf("failed to write file (%v)", err) + } + + repos, err := LoadFile(file.Name()) + if err != nil { + t.Errorf("failed to load file (%v)", err) + } + for _, repo := range sampleRepository.Repositories { + if !repos.Has(repo.Name) { + t.Errorf("expected repository %s not found", repo.Name) + } + } +} + +func TestRepoNotExists(t *testing.T) { + if _, err := LoadFile("/this/path/does/not/exist.yaml"); err == nil { + t.Errorf("expected err to be non-nil when path does not exist") + } else if !strings.Contains(err.Error(), "couldn't load repositories file") { + t.Errorf("expected prompt `couldn't load repositories file`") + } +} + +func TestRemoveRepositoryInvalidEntries(t *testing.T) { + sampleRepository := NewFile() + sampleRepository.Add( + &Entry{ + Name: "stable", + URL: "https://example.com/stable/charts", + }, + &Entry{ + Name: "incubator", + URL: "https://example.com/incubator", + }, + &Entry{}, + nil, + &Entry{ + Name: "test", + URL: "https://example.com/test", + }, + ) + + removeRepository := "stable" + found := sampleRepository.Remove(removeRepository) + if !found { + t.Errorf("expected repository %s not found", removeRepository) + } + + found = sampleRepository.Has(removeRepository) + if found { + t.Errorf("repository %s not deleted", removeRepository) + } +} diff --git a/pkg/helm/pkg/repo/repotest/doc.go b/pkg/helm/pkg/repo/v1/repotest/doc.go similarity index 100% rename from pkg/helm/pkg/repo/repotest/doc.go rename to pkg/helm/pkg/repo/v1/repotest/doc.go diff --git a/pkg/helm/pkg/repo/v1/repotest/server.go b/pkg/helm/pkg/repo/v1/repotest/server.go new file mode 100644 index 00000000..f098158d --- /dev/null +++ b/pkg/helm/pkg/repo/v1/repotest/server.go @@ -0,0 +1,425 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" // used for docker test registry + _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" // used for docker test registry + "golang.org/x/crypto/bcrypt" + "sigs.k8s.io/yaml" + + chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + "github.com/werf/nelm/pkg/helm/pkg/chart/v2/loader" + chartutil "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" + ociRegistry "github.com/werf/nelm/pkg/helm/pkg/registry" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +func BasicAuthMiddleware(t *testing.T) http.HandlerFunc { + t.Helper() + return http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if !ok || username != "username" || password != "password" { + t.Errorf("Expected request to use basic auth and for username == 'username' and password == 'password', got '%v', '%s', '%s'", ok, username, password) + } + }) +} + +type ServerOption func(*testing.T, *Server) + +func WithTLSConfig(tlsConfig *tls.Config) ServerOption { + return func(_ *testing.T, server *Server) { + server.tlsConfig = tlsConfig + } +} + +func WithMiddleware(middleware http.HandlerFunc) ServerOption { + return func(_ *testing.T, server *Server) { + server.middleware = middleware + } +} + +func WithChartSourceGlob(glob string) ServerOption { + return func(_ *testing.T, server *Server) { + server.chartSourceGlob = glob + } +} + +// Server is an implementation of a repository server for testing. +type Server struct { + docroot string + srv *httptest.Server + middleware http.HandlerFunc + tlsConfig *tls.Config + chartSourceGlob string +} + +// NewTempServer creates a server inside of a temp dir. +// +// If the passed in string is not "", it will be treated as a shell glob, and files +// will be copied from that path to the server's docroot. +// +// The server is started automatically. The caller is responsible for stopping +// the server. +// +// The temp dir will be removed by testing package automatically when test finished. +func NewTempServer(t *testing.T, options ...ServerOption) *Server { + t.Helper() + docrootTempDir := t.TempDir() + + srv := newServer(t, docrootTempDir, options...) + + t.Cleanup(func() { os.RemoveAll(srv.docroot) }) + + if srv.chartSourceGlob != "" { + if _, err := srv.CopyCharts(srv.chartSourceGlob); err != nil { + t.Fatal(err) + } + } + + return srv +} + +// Create the server, but don't yet start it +func newServer(t *testing.T, docroot string, options ...ServerOption) *Server { + t.Helper() + absdocroot, err := filepath.Abs(docroot) + if err != nil { + t.Fatal(err) + } + + s := &Server{ + docroot: absdocroot, + } + + for _, option := range options { + option(t, s) + } + + s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.middleware != nil { + s.middleware.ServeHTTP(w, r) + } + http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r) + })) + + s.start() + + // Add the testing repository as the only repo. Server must be started for the server's URL to be valid + if err := setTestingRepository(s.URL(), filepath.Join(s.docroot, "repositories.yaml")); err != nil { + t.Fatal(err) + } + + return s +} + +type OCIServer struct { + *registry.Registry + RegistryURL string + Dir string + TestUsername string + TestPassword string + Client *ociRegistry.Client +} + +type OCIServerRunConfig struct { + DependingChart *chart.Chart +} + +type OCIServerOpt func(config *OCIServerRunConfig) + +type OCIServerRunResult struct { + PushedChart *ociRegistry.PushResult +} + +func WithDependingChart(c *chart.Chart) OCIServerOpt { + return func(config *OCIServerRunConfig) { + config.DependingChart = c + } +} + +func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { + t.Helper() + testHtpasswdFileBasename := "authtest.htpasswd" + testUsername, testPassword := "username", "password" + + pwBytes, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.DefaultCost) + if err != nil { + t.Fatal("error generating bcrypt password for test htpasswd file") + } + htpasswdPath := filepath.Join(dir, testHtpasswdFileBasename) + err = os.WriteFile(htpasswdPath, fmt.Appendf(nil, "%s:%s\n", testUsername, string(pwBytes)), 0o644) + if err != nil { + t.Fatalf("error creating test htpasswd file") + } + + // Registry config + config := &configuration.Configuration{} + ln, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("error finding free port for test registry") + } + defer ln.Close() + + port := ln.Addr().(*net.TCPAddr).Port + config.HTTP.Addr = ln.Addr().String() + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + config.Auth = configuration.Auth{ + "htpasswd": configuration.Parameters{ + "realm": "localhost", + "path": htpasswdPath, + }, + } + + registryURL := fmt.Sprintf("localhost:%d", port) + + r, err := registry.NewRegistry(t.Context(), config) + if err != nil { + t.Fatal(err) + } + + return &OCIServer{ + Registry: r, + RegistryURL: registryURL, + TestUsername: testUsername, + TestPassword: testPassword, + Dir: dir, + }, nil +} + +func (srv *OCIServer) Run(t *testing.T, opts ...OCIServerOpt) { + t.Helper() + _ = srv.RunWithReturn(t, opts...) +} + +func (srv *OCIServer) RunWithReturn(t *testing.T, opts ...OCIServerOpt) *OCIServerRunResult { + t.Helper() + cfg := &OCIServerRunConfig{} + for _, fn := range opts { + fn(cfg) + } + + go srv.ListenAndServe() + + credentialsFile := filepath.Join(srv.Dir, "config.json") + + // init test client + registryClient, err := ociRegistry.NewClient( + ociRegistry.ClientOptDebug(true), + ociRegistry.ClientOptEnableCache(true), + ociRegistry.ClientOptWriter(os.Stdout), + ociRegistry.ClientOptCredentialsFile(credentialsFile), + ) + if err != nil { + t.Fatalf("error creating registry client") + } + + err = registryClient.Login( + srv.RegistryURL, + ociRegistry.LoginOptBasicAuth(srv.TestUsername, srv.TestPassword), + ociRegistry.LoginOptInsecure(true), + ociRegistry.LoginOptPlainText(true)) + if err != nil { + t.Fatalf("error logging into registry with good credentials: %v", err) + } + + ref := fmt.Sprintf("%s/u/ocitestuser/oci-dependent-chart:0.1.0", srv.RegistryURL) + + err = chartutil.ExpandFile(srv.Dir, filepath.Join(srv.Dir, "oci-dependent-chart-0.1.0.tgz")) + if err != nil { + t.Fatal(err) + } + + // valid chart + ch, err := loader.LoadDir(context.Background(), filepath.Join(srv.Dir, "oci-dependent-chart")) + if err != nil { + t.Fatal("error loading chart") + } + + err = os.RemoveAll(filepath.Join(srv.Dir, "oci-dependent-chart")) + if err != nil { + t.Fatal("error removing chart before push") + } + + // save it back to disk.. + absPath, err := chartutil.Save(ch, srv.Dir) + if err != nil { + t.Fatal("could not create chart archive") + } + + // load it into memory... + contentBytes, err := os.ReadFile(absPath) + if err != nil { + t.Fatal("could not load chart into memory") + } + + result, err := registryClient.Push(contentBytes, ref) + if err != nil { + t.Fatalf("error pushing dependent chart: %s", err) + } + t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ + "Config.Digest: %s, Config.Size: %d, "+ + "Chart.Digest: %s, Chart.Size: %d", + result.Manifest.Digest, result.Manifest.Size, + result.Config.Digest, result.Config.Size, + result.Chart.Digest, result.Chart.Size) + + srv.Client = registryClient + c := cfg.DependingChart + if c == nil { + return &OCIServerRunResult{ + PushedChart: result, + } + } + + dependingRef := fmt.Sprintf("%s/u/ocitestuser/%s:%s", + srv.RegistryURL, c.Metadata.Name, c.Metadata.Version) + + // load it into memory... + absPath = filepath.Join(srv.Dir, + fmt.Sprintf("%s-%s.tgz", c.Metadata.Name, c.Metadata.Version)) + contentBytes, err = os.ReadFile(absPath) + if err != nil { + t.Fatal("could not load chart into memory") + } + + result, err = registryClient.Push(contentBytes, dependingRef) + if err != nil { + t.Fatalf("error pushing depending chart: %s", err) + } + t.Logf("Manifest.Digest: %s, Manifest.Size: %d, "+ + "Config.Digest: %s, Config.Size: %d, "+ + "Chart.Digest: %s, Chart.Size: %d", + result.Manifest.Digest, result.Manifest.Size, + result.Config.Digest, result.Config.Size, + result.Chart.Digest, result.Chart.Size) + + return &OCIServerRunResult{ + PushedChart: result, + } +} + +// Root gets the docroot for the server. +func (s *Server) Root() string { + return s.docroot +} + +// CopyCharts takes a glob expression and copies those charts to the server root. +func (s *Server) CopyCharts(origin string) ([]string, error) { + files, err := filepath.Glob(origin) + if err != nil { + return []string{}, err + } + copied := make([]string, len(files)) + for i, f := range files { + base := filepath.Base(f) + newname := filepath.Join(s.docroot, base) + data, err := os.ReadFile(f) + if err != nil { + return []string{}, err + } + if err := os.WriteFile(newname, data, 0o644); err != nil { + return []string{}, err + } + copied[i] = newname + } + + err = s.CreateIndex() + return copied, err +} + +// CreateIndex will read docroot and generate an index.yaml file. +func (s *Server) CreateIndex() error { + // generate the index + index, err := repo.IndexDirectory(s.docroot, s.URL()) + if err != nil { + return err + } + + d, err := yaml.Marshal(index) + if err != nil { + return err + } + + ifile := filepath.Join(s.docroot, "index.yaml") + return os.WriteFile(ifile, d, 0o644) +} + +func (s *Server) start() { + if s.tlsConfig != nil { + s.srv.TLS = s.tlsConfig + s.srv.StartTLS() + } else { + s.srv.Start() + } +} + +// Stop stops the server and closes all connections. +// +// It should be called explicitly. +func (s *Server) Stop() { + s.srv.Close() +} + +// URL returns the URL of the server. +// +// Example: +// +// http://localhost:1776 +func (s *Server) URL() string { + return s.srv.URL +} + +func (s *Server) Client() *http.Client { + return s.srv.Client() +} + +// LinkIndices links the index created with CreateIndex and makes a symbolic link to the cache index. +// +// This makes it possible to simulate a local cache of a repository. +func (s *Server) LinkIndices() error { + lstart := filepath.Join(s.docroot, "index.yaml") + ldest := filepath.Join(s.docroot, "test-index.yaml") + return os.Symlink(lstart, ldest) +} + +// setTestingRepository sets up a testing repository.yaml with only the given URL. +func setTestingRepository(url, fname string) error { + if url == "" { + panic("no url") + } + + r := repo.NewFile() + r.Add(&repo.Entry{ + Name: "test", + URL: url, + }) + return r.WriteFile(fname, 0o640) +} diff --git a/pkg/helm/pkg/repo/v1/repotest/server_test.go b/pkg/helm/pkg/repo/v1/repotest/server_test.go new file mode 100644 index 00000000..1907b014 --- /dev/null +++ b/pkg/helm/pkg/repo/v1/repotest/server_test.go @@ -0,0 +1,222 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest + +import ( + "io" + "net/http" + "path/filepath" + "strings" + "testing" + + "sigs.k8s.io/yaml" + + "github.com/werf/nelm/pkg/helm/intern/test/ensure" + "github.com/werf/nelm/pkg/helm/pkg/repo/v1" +) + +// Young'n, in these here parts, we test our tests. + +func TestServer(t *testing.T) { + ensure.HelmHome(t) + + rootDir := t.TempDir() + + srv := newServer(t, rootDir) + defer srv.Stop() + + c, err := srv.CopyCharts("testdata/*.tgz") + if err != nil { + // Some versions of Go don't correctly fire defer on Fatal. + t.Fatal(err) + } + + if len(c) != 1 { + t.Errorf("Unexpected chart count: %d", len(c)) + } + + if filepath.Base(c[0]) != "examplechart-0.1.0.tgz" { + t.Errorf("Unexpected chart: %s", c[0]) + } + + res, err := http.Get(srv.URL() + "/examplechart-0.1.0.tgz") + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + if res.ContentLength < 500 { + t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) + } + + res, err = http.Get(srv.URL() + "/index.yaml") + if err != nil { + t.Fatal(err) + } + + data, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + m := repo.NewIndexFile() + if err := yaml.Unmarshal(data, m); err != nil { + t.Fatal(err) + } + + if l := len(m.Entries); l != 1 { + t.Fatalf("Expected 1 entry, got %d", l) + } + + expect := "examplechart" + if !m.Has(expect, "0.1.0") { + t.Errorf("missing %q", expect) + } + + res, err = http.Get(srv.URL() + "/index.yaml-nosuchthing") + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusNotFound { + t.Fatalf("Expected 404, got %d", res.StatusCode) + } +} + +func TestNewTempServer(t *testing.T) { + ensure.HelmHome(t) + + type testCase struct { + options []ServerOption + } + + testCases := map[string]testCase{ + "plainhttp": { + options: []ServerOption{ + WithChartSourceGlob("testdata/examplechart-0.1.0.tgz"), + }, + }, + "tls": { + options: []ServerOption{ + WithChartSourceGlob("testdata/examplechart-0.1.0.tgz"), + WithTLSConfig(MakeTestTLSConfig(t, "../../../../testdata")), + }, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + srv := NewTempServer( + t, + tc.options..., + ) + defer srv.Stop() + + if srv.srv.URL == "" { + t.Fatal("unstarted server") + } + + client := srv.Client() + + { + res, err := client.Head(srv.URL() + "/repositories.yaml") + if err != nil { + t.Error(err) + } + + res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Errorf("Expected 200, got %d", res.StatusCode) + } + + } + + { + res, err := client.Head(srv.URL() + "/examplechart-0.1.0.tgz") + if err != nil { + t.Error(err) + } + res.Body.Close() + + if res.StatusCode != http.StatusOK { + t.Errorf("Expected 200, got %d", res.StatusCode) + } + } + + res, err := client.Get(srv.URL() + "/examplechart-0.1.0.tgz") + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + if res.ContentLength < 500 { + t.Errorf("Expected at least 500 bytes of data, got %d", res.ContentLength) + } + + res, err = client.Get(srv.URL() + "/index.yaml") + if err != nil { + t.Fatal(err) + } + + data, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + + m := repo.NewIndexFile() + if err := yaml.Unmarshal(data, m); err != nil { + t.Fatal(err) + } + + if l := len(m.Entries); l != 1 { + t.Fatalf("Expected 1 entry, got %d", l) + } + + expect := "examplechart" + if !m.Has(expect, "0.1.0") { + t.Errorf("missing %q", expect) + } + + res, err = client.Get(srv.URL() + "/index.yaml-nosuchthing") + res.Body.Close() + if err != nil { + t.Fatal(err) + } + if res.StatusCode != http.StatusNotFound { + t.Fatalf("Expected 404, got %d", res.StatusCode) + } + }) + } + +} + +func TestNewTempServer_TLS(t *testing.T) { + ensure.HelmHome(t) + + srv := NewTempServer( + t, + WithChartSourceGlob("testdata/examplechart-0.1.0.tgz"), + WithTLSConfig(MakeTestTLSConfig(t, "../../../../testdata")), + ) + defer srv.Stop() + + if !strings.HasPrefix(srv.URL(), "https://") { + t.Fatal("non-TLS server") + } +} diff --git a/pkg/helm/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz b/pkg/helm/pkg/repo/v1/repotest/testdata/examplechart-0.1.0.tgz similarity index 100% rename from pkg/helm/pkg/repo/repotest/testdata/examplechart-0.1.0.tgz rename to pkg/helm/pkg/repo/v1/repotest/testdata/examplechart-0.1.0.tgz diff --git a/pkg/helm/pkg/repo/repotest/testdata/examplechart/.helmignore b/pkg/helm/pkg/repo/v1/repotest/testdata/examplechart/.helmignore similarity index 100% rename from pkg/helm/pkg/repo/repotest/testdata/examplechart/.helmignore rename to pkg/helm/pkg/repo/v1/repotest/testdata/examplechart/.helmignore diff --git a/pkg/helm/pkg/repo/repotest/testdata/examplechart/Chart.yaml b/pkg/helm/pkg/repo/v1/repotest/testdata/examplechart/Chart.yaml similarity index 100% rename from pkg/helm/pkg/repo/repotest/testdata/examplechart/Chart.yaml rename to pkg/helm/pkg/repo/v1/repotest/testdata/examplechart/Chart.yaml diff --git a/pkg/helm/pkg/repo/repotest/testdata/examplechart/values.yaml b/pkg/helm/pkg/repo/v1/repotest/testdata/examplechart/values.yaml similarity index 100% rename from pkg/helm/pkg/repo/repotest/testdata/examplechart/values.yaml rename to pkg/helm/pkg/repo/v1/repotest/testdata/examplechart/values.yaml diff --git a/pkg/helm/pkg/repo/v1/repotest/tlsconfig.go b/pkg/helm/pkg/repo/v1/repotest/tlsconfig.go new file mode 100644 index 00000000..30aa28bf --- /dev/null +++ b/pkg/helm/pkg/repo/v1/repotest/tlsconfig.go @@ -0,0 +1,44 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package repotest + +import ( + "crypto/tls" + "path/filepath" + "testing" + + "github.com/werf/nelm/pkg/helm/intern/tlsutil" + + "github.com/stretchr/testify/require" +) + +func MakeTestTLSConfig(t *testing.T, path string) *tls.Config { + t.Helper() + ca, pub, priv := filepath.Join(path, "rootca.crt"), filepath.Join(path, "crt.pem"), filepath.Join(path, "key.pem") + + insecure := false + tlsConf, err := tlsutil.NewTLSConfig( + tlsutil.WithInsecureSkipVerify(insecure), + tlsutil.WithCertKeyPairFiles(pub, priv), + tlsutil.WithCAFile(ca), + ) + //require.Nil(t, err, err.Error()) + require.Nil(t, err) + + tlsConf.ServerName = "helm.sh" + + return tlsConf +} diff --git a/pkg/helm/pkg/repo/testdata/chartmuseum-index.yaml b/pkg/helm/pkg/repo/v1/testdata/chartmuseum-index.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/chartmuseum-index.yaml rename to pkg/helm/pkg/repo/v1/testdata/chartmuseum-index.yaml diff --git a/pkg/helm/pkg/repo/testdata/local-index-annotations.yaml b/pkg/helm/pkg/repo/v1/testdata/local-index-annotations.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/local-index-annotations.yaml rename to pkg/helm/pkg/repo/v1/testdata/local-index-annotations.yaml diff --git a/pkg/helm/pkg/repo/testdata/local-index-unordered.yaml b/pkg/helm/pkg/repo/v1/testdata/local-index-unordered.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/local-index-unordered.yaml rename to pkg/helm/pkg/repo/v1/testdata/local-index-unordered.yaml diff --git a/pkg/helm/pkg/repo/testdata/local-index.json b/pkg/helm/pkg/repo/v1/testdata/local-index.json similarity index 100% rename from pkg/helm/pkg/repo/testdata/local-index.json rename to pkg/helm/pkg/repo/v1/testdata/local-index.json diff --git a/pkg/helm/pkg/repo/testdata/local-index.yaml b/pkg/helm/pkg/repo/v1/testdata/local-index.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/local-index.yaml rename to pkg/helm/pkg/repo/v1/testdata/local-index.yaml diff --git a/pkg/helm/pkg/repo/testdata/old-repositories.yaml b/pkg/helm/pkg/repo/v1/testdata/old-repositories.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/old-repositories.yaml rename to pkg/helm/pkg/repo/v1/testdata/old-repositories.yaml diff --git a/pkg/helm/pkg/repo/testdata/repositories.yaml b/pkg/helm/pkg/repo/v1/testdata/repositories.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/repositories.yaml rename to pkg/helm/pkg/repo/v1/testdata/repositories.yaml diff --git a/pkg/helm/pkg/repo/v1/testdata/repository/frobnitz-1.2.3.tgz b/pkg/helm/pkg/repo/v1/testdata/repository/frobnitz-1.2.3.tgz new file mode 100644 index 00000000..8731dce0 Binary files /dev/null and b/pkg/helm/pkg/repo/v1/testdata/repository/frobnitz-1.2.3.tgz differ diff --git a/pkg/helm/pkg/repo/testdata/repository/sprocket-1.1.0.tgz b/pkg/helm/pkg/repo/v1/testdata/repository/sprocket-1.1.0.tgz similarity index 100% rename from pkg/helm/pkg/repo/testdata/repository/sprocket-1.1.0.tgz rename to pkg/helm/pkg/repo/v1/testdata/repository/sprocket-1.1.0.tgz diff --git a/pkg/helm/pkg/repo/testdata/repository/sprocket-1.2.0.tgz b/pkg/helm/pkg/repo/v1/testdata/repository/sprocket-1.2.0.tgz similarity index 100% rename from pkg/helm/pkg/repo/testdata/repository/sprocket-1.2.0.tgz rename to pkg/helm/pkg/repo/v1/testdata/repository/sprocket-1.2.0.tgz diff --git a/pkg/helm/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz b/pkg/helm/pkg/repo/v1/testdata/repository/universe/zarthal-1.0.0.tgz similarity index 100% rename from pkg/helm/pkg/repo/testdata/repository/universe/zarthal-1.0.0.tgz rename to pkg/helm/pkg/repo/v1/testdata/repository/universe/zarthal-1.0.0.tgz diff --git a/pkg/helm/pkg/repo/testdata/server/index.yaml b/pkg/helm/pkg/repo/v1/testdata/server/index.yaml similarity index 100% rename from pkg/helm/pkg/repo/testdata/server/index.yaml rename to pkg/helm/pkg/repo/v1/testdata/server/index.yaml diff --git a/pkg/helm/pkg/repo/testdata/server/test.txt b/pkg/helm/pkg/repo/v1/testdata/server/test.txt similarity index 100% rename from pkg/helm/pkg/repo/testdata/server/test.txt rename to pkg/helm/pkg/repo/v1/testdata/server/test.txt diff --git a/pkg/helm/pkg/storage/driver/cfgmaps.go b/pkg/helm/pkg/storage/driver/cfgmaps.go index 96629baf..e94eda64 100644 --- a/pkg/helm/pkg/storage/driver/cfgmaps.go +++ b/pkg/helm/pkg/storage/driver/cfgmaps.go @@ -14,15 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "context" + "fmt" + "log/slog" "strconv" "strings" "time" - "github.com/pkg/errors" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,7 +31,9 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) var _ Driver = (*ConfigMaps)(nil) @@ -42,16 +45,19 @@ const ConfigMapsDriverName = "ConfigMap" // ConfigMapsInterface. type ConfigMaps struct { impl corev1.ConfigMapInterface - Log func(string, ...interface{}) + + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of // the kubernetes ConfigMapsInterface. func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps { - return &ConfigMaps{ + c := &ConfigMaps{ impl: impl, - Log: func(_ string, _ ...interface{}) {}, } + c.SetLogger(slog.Default().Handler()) + return c } // Name returns the name of the driver. @@ -61,7 +67,7 @@ func (cfgmaps *ConfigMaps) Name() string { // Get fetches the release named by key. The corresponding release is returned // or error if not found. -func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { +func (cfgmaps *ConfigMaps) Get(key string) (release.Releaser, error) { // fetch the configmap holding the release named by key obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { @@ -69,16 +75,16 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { return nil, ErrReleaseNotFound } - cfgmaps.Log("get: failed to get %q: %s", key, err) + cfgmaps.Logger().Debug("failed to get release", slog.String("key", key), slog.Any("error", err)) return nil, err } // found the configmap, decode the base64 data string r, err := decodeRelease(obj.Data["release"]) if err != nil { - cfgmaps.Log("get: failed to decode data %q: %s", key, err) + cfgmaps.Logger().Debug("failed to decode data", slog.String("key", key), slog.Any("error", err)) return nil, err } - r.Labels = filterSystemLabels(obj.ObjectMeta.Labels) + r.Labels = filterSystemLabels(obj.Labels) // return the release object return r, nil } @@ -86,28 +92,28 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // configmap fails to retrieve the releases. -func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (cfgmaps *ConfigMaps) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { - cfgmaps.Log("list: failed to list: %s", err) + cfgmaps.Logger().Debug("failed to list releases", slog.Any("error", err)) return nil, err } - var results []*rspb.Release + var results []release.Releaser // iterate over the configmaps object list // and decode each release for _, item := range list.Items { rls, err := decodeRelease(item.Data["release"]) if err != nil { - cfgmaps.Log("list: failed to decode release: %v: %s", item, err) + cfgmaps.Logger().Debug("failed to decode release", slog.Any("item", item), slog.Any("error", err)) continue } - rls.Labels = item.ObjectMeta.Labels + rls.Labels = item.Labels if filter(rls) { results = append(results, rls) @@ -118,11 +124,11 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas // Query fetches all releases that match the provided map of labels. // An error is returned if the configmap fails to retrieve the releases. -func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) { +func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]release.Releaser, error) { ls := kblabels.Set{} for k, v := range labels { if errs := validation.IsValidLabelValue(v); len(errs) != 0 { - return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) + return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) } ls[k] = v } @@ -131,7 +137,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { - cfgmaps.Log("query: failed to query with labels: %s", err) + cfgmaps.Logger().Debug("failed to query with labels", slog.Any("error", err)) return nil, err } @@ -139,14 +145,14 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err return nil, ErrReleaseNotFound } - var results []*rspb.Release + var results []release.Releaser for _, item := range list.Items { rls, err := decodeRelease(item.Data["release"]) if err != nil { - cfgmaps.Log("query: failed to decode release: %s", err) + cfgmaps.Logger().Debug("failed to decode release", slog.Any("error", err)) continue } - rls.Labels = item.ObjectMeta.Labels + rls.Labels = item.Labels results = append(results, rls) } return results, nil @@ -154,18 +160,28 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err // Create creates a new ConfigMap holding the release. If the // ConfigMap already exists, ErrReleaseExists is returned. -func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { +func (cfgmaps *ConfigMaps) Create(key string, rls release.Releaser) error { // set labels for configmaps object meta data var lbs labels + rac, err := release.NewAccessor(rls) + if err != nil { + return err + } + lbs.init() - lbs.fromMap(rls.Labels) - lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) + lbs.fromMap(rac.Labels()) + lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix())) + + rel, err := releaserToV1Release(rls) + if err != nil { + return err + } // create a new configmap to hold the release - obj, err := newConfigMapsObject(key, rls, lbs) + obj, err := newConfigMapsObject(key, rel, lbs) if err != nil { - cfgmaps.Log("create: failed to encode release %q: %s", rls.Name, err) + cfgmaps.Logger().Debug("failed to encode release", slog.String("name", rac.Name()), slog.Any("error", err)) return err } // push the configmap object out into the kubiverse @@ -174,7 +190,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { return ErrReleaseExists } - cfgmaps.Log("create: failed to create: %s", err) + cfgmaps.Logger().Debug("failed to create release", slog.Any("error", err)) return err } return nil @@ -182,31 +198,40 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { // Update updates the ConfigMap holding the release. If not found // the ConfigMap is created to hold the release. -func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { +func (cfgmaps *ConfigMaps) Update(key string, rel release.Releaser) error { // set labels for configmaps object meta data var lbs labels + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + lbs.init() lbs.fromMap(rls.Labels) - lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix())) // create a new configmap object to hold the release obj, err := newConfigMapsObject(key, rls, lbs) if err != nil { - cfgmaps.Log("update: failed to encode release %q: %s", rls.Name, err) + cfgmaps.Logger().Debug( + "failed to encode release", + slog.String("name", rls.Name), + slog.Any("error", err), + ) return err } // push the configmap object out into the kubiverse _, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) if err != nil { - cfgmaps.Log("update: failed to update: %s", err) + cfgmaps.Logger().Debug("failed to update release", slog.Any("error", err)) return err } return nil } // Delete deletes the ConfigMap holding the release named by key. -func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { +func (cfgmaps *ConfigMaps) Delete(key string) (rls release.Releaser, err error) { // fetch the release to check existence if rls, err = cfgmaps.Get(key); err != nil { return nil, err diff --git a/pkg/helm/pkg/storage/driver/cfgmaps_test.go b/pkg/helm/pkg/storage/driver/cfgmaps_test.go index 5eae389d..4040d2d6 100644 --- a/pkg/helm/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/helm/pkg/storage/driver/cfgmaps_test.go @@ -16,12 +16,15 @@ package driver import ( "encoding/base64" "encoding/json" + "errors" "reflect" "testing" v1 "k8s.io/api/core/v1" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) func TestConfigMapName(t *testing.T) { @@ -36,7 +39,7 @@ func TestConfigMapGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) @@ -56,7 +59,7 @@ func TestUncompressedConfigMapGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // Create a test fixture which contains an uncompressed release cfgmap, err := newConfigMapsObject(key, rel, nil) @@ -83,19 +86,35 @@ func TestUncompressedConfigMapGet(t *testing.T) { } } +func convertReleaserToV1(t *testing.T, rel release.Releaser) *rspb.Release { + t.Helper() + switch r := rel.(type) { + case rspb.Release: + return &r + case *rspb.Release: + return r + case nil: + return nil + } + + t.Fatalf("Unsupported release type: %T", rel) + return nil +} + func TestConfigMapList(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) // list all deleted releases - del, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := cfgmaps.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -106,8 +125,9 @@ func TestConfigMapList(t *testing.T) { } // list all deployed releases - dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := cfgmaps.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -118,8 +138,9 @@ func TestConfigMapList(t *testing.T) { } // list all superseded releases - ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := cfgmaps.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -129,7 +150,7 @@ func TestConfigMapList(t *testing.T) { t.Errorf("Expected 2 superseded, got %d", len(ssd)) } // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. - rls := ssd[0] + rls := convertReleaserToV1(t, ssd[0]) _, ok := rls.Labels["name"] if !ok { t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) @@ -142,12 +163,12 @@ func TestConfigMapList(t *testing.T) { func TestConfigMapQuery(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) rls, err := cfgmaps.Query(map[string]string{"status": "deployed"}) @@ -171,7 +192,7 @@ func TestConfigMapCreate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // store the release in a configmap if err := cfgmaps.Create(key, rel); err != nil { @@ -195,12 +216,12 @@ func TestConfigMapUpdate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) // modify release status code - rel.Info.Status = rspb.StatusSuperseded + rel.Info.Status = common.StatusSuperseded // perform the update if err := cfgmaps.Update(key, rel); err != nil { @@ -208,10 +229,11 @@ func TestConfigMapUpdate(t *testing.T) { } // fetch the updated release - got, err := cfgmaps.Get(key) + goti, err := cfgmaps.Get(key) if err != nil { t.Fatalf("Failed to get release with key %q: %s", key, err) } + got := convertReleaserToV1(t, goti) // check release has actually been updated by comparing modified fields if rel.Info.Status != got.Info.Status { @@ -224,7 +246,7 @@ func TestConfigMapDelete(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) @@ -242,10 +264,8 @@ func TestConfigMapDelete(t *testing.T) { if !reflect.DeepEqual(rel, rls) { t.Errorf("Expected {%v}, got {%v}", rel, rls) } - - // fetch the deleted release _, err = cfgmaps.Get(key) - if !reflect.DeepEqual(ErrReleaseNotFound, err) { + if !errors.Is(err, ErrReleaseNotFound) { t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) } } diff --git a/pkg/helm/pkg/storage/driver/driver.go b/pkg/helm/pkg/storage/driver/driver.go index ebf3415a..0dc8a0ac 100644 --- a/pkg/helm/pkg/storage/driver/driver.go +++ b/pkg/helm/pkg/storage/driver/driver.go @@ -14,14 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( + "errors" "fmt" - "github.com/pkg/errors" - - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) var ( @@ -59,7 +59,7 @@ func NewErrNoDeployedReleases(releaseName string) error { // Create stores the release or returns ErrReleaseExists // if an identical release already exists. type Creator interface { - Create(key string, rls *rspb.Release) error + Create(key string, rls release.Releaser) error } // Updator is the interface that wraps the Update method. @@ -67,7 +67,7 @@ type Creator interface { // Update updates an existing release or returns // ErrReleaseNotFound if the release does not exist. type Updator interface { - Update(key string, rls *rspb.Release) error + Update(key string, rls release.Releaser) error } // Deletor is the interface that wraps the Delete method. @@ -75,7 +75,7 @@ type Updator interface { // Delete deletes the release named by key or returns // ErrReleaseNotFound if the release does not exist. type Deletor interface { - Delete(key string) (*rspb.Release, error) + Delete(key string) (release.Releaser, error) } // Queryor is the interface that wraps the Get and List methods. @@ -87,9 +87,9 @@ type Deletor interface { // // Query returns the set of all releases that match the provided label set. type Queryor interface { - Get(key string) (*rspb.Release, error) - List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) - Query(labels map[string]string) ([]*rspb.Release, error) + Get(key string) (release.Releaser, error) + List(filter func(release.Releaser) bool) ([]release.Releaser, error) + Query(labels map[string]string) ([]release.Releaser, error) } // Driver is the interface composed of Creator, Updator, Deletor, and Queryor @@ -103,3 +103,18 @@ type Driver interface { Queryor Name() string } + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { + switch r := rel.(type) { + case rspb.Release: + return &r, nil + case *rspb.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} diff --git a/pkg/helm/pkg/storage/driver/labels_test.go b/pkg/helm/pkg/storage/driver/labels_test.go index bfd80911..dff6e905 100644 --- a/pkg/helm/pkg/storage/driver/labels_test.go +++ b/pkg/helm/pkg/storage/driver/labels_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "testing" diff --git a/pkg/helm/pkg/storage/driver/memory.go b/pkg/helm/pkg/storage/driver/memory.go index 3340ff61..586771e1 100644 --- a/pkg/helm/pkg/storage/driver/memory.go +++ b/pkg/helm/pkg/storage/driver/memory.go @@ -17,11 +17,13 @@ limitations under the License. package driver import ( + "log/slog" "strconv" "strings" "sync" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/release" ) var _ Driver = (*Memory)(nil) @@ -42,11 +44,15 @@ type Memory struct { namespace string // A map of namespaces to releases cache map[string]memReleases + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // NewMemory initializes a new memory driver. func NewMemory() *Memory { - return &Memory{cache: map[string]memReleases{}, namespace: "default"} + m := &Memory{cache: map[string]memReleases{}, namespace: "default"} + m.SetLogger(slog.Default().Handler()) + return m } // SetNamespace sets a specific namespace in which releases will be accessed. @@ -61,7 +67,7 @@ func (mem *Memory) Name() string { } // Get returns the release named by key or returns ErrReleaseNotFound. -func (mem *Memory) Get(key string) (*rspb.Release, error) { +func (mem *Memory) Get(key string) (release.Releaser, error) { defer unlock(mem.rlock()) keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") @@ -83,10 +89,10 @@ func (mem *Memory) Get(key string) (*rspb.Release, error) { } // List returns the list of all releases such that filter(release) == true -func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (mem *Memory) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { defer unlock(mem.rlock()) - var ls []*rspb.Release + var ls []release.Releaser for namespace := range mem.cache { if mem.namespace != "" { // Should only list releases of this namespace @@ -109,7 +115,7 @@ func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error } // Query returns the set of releases that match the provided set of labels -func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { +func (mem *Memory) Query(keyvals map[string]string) ([]release.Releaser, error) { defer unlock(mem.rlock()) var lbs labels @@ -117,7 +123,7 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { lbs.init() lbs.fromMap(keyvals) - var ls []*rspb.Release + var ls []release.Releaser for namespace := range mem.cache { if mem.namespace != "" { // Should only query releases of this namespace @@ -150,9 +156,13 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { } // Create creates a new release or returns ErrReleaseExists. -func (mem *Memory) Create(key string, rls *rspb.Release) error { +func (mem *Memory) Create(key string, rel release.Releaser) error { defer unlock(mem.wlock()) + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } // For backwards compatibility, we protect against an unset namespace namespace := rls.Namespace if namespace == "" { @@ -176,9 +186,14 @@ func (mem *Memory) Create(key string, rls *rspb.Release) error { } // Update updates a release or returns ErrReleaseNotFound. -func (mem *Memory) Update(key string, rls *rspb.Release) error { +func (mem *Memory) Update(key string, rel release.Releaser) error { defer unlock(mem.wlock()) + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + // For backwards compatibility, we protect against an unset namespace namespace := rls.Namespace if namespace == "" { @@ -196,7 +211,7 @@ func (mem *Memory) Update(key string, rls *rspb.Release) error { } // Delete deletes a release or returns ErrReleaseNotFound. -func (mem *Memory) Delete(key string) (*rspb.Release, error) { +func (mem *Memory) Delete(key string) (release.Releaser, error) { defer unlock(mem.wlock()) keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") diff --git a/pkg/helm/pkg/storage/driver/memory_test.go b/pkg/helm/pkg/storage/driver/memory_test.go index 6728b2c7..268e15a8 100644 --- a/pkg/helm/pkg/storage/driver/memory_test.go +++ b/pkg/helm/pkg/storage/driver/memory_test.go @@ -21,7 +21,11 @@ import ( "reflect" "testing" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/stretchr/testify/assert" + + "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) func TestMemoryName(t *testing.T) { @@ -38,22 +42,22 @@ func TestMemoryCreate(t *testing.T) { }{ { "create should succeed", - releaseStub("rls-c", 1, "default", rspb.StatusDeployed), + releaseStub("rls-c", 1, "default", common.StatusDeployed), false, }, { "create should fail (release already exists)", - releaseStub("rls-a", 1, "default", rspb.StatusDeployed), + releaseStub("rls-a", 1, "default", common.StatusDeployed), true, }, { "create in namespace should succeed", - releaseStub("rls-a", 1, "mynamespace", rspb.StatusDeployed), + releaseStub("rls-a", 1, "mynamespace", common.StatusDeployed), false, }, { "create in other namespace should fail (release already exists)", - releaseStub("rls-c", 1, "mynamespace", rspb.StatusDeployed), + releaseStub("rls-c", 1, "mynamespace", common.StatusDeployed), true, }, } @@ -104,8 +108,9 @@ func TestMemoryList(t *testing.T) { ts.SetNamespace("default") // list all deployed releases - dpl, err := ts.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := ts.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -116,8 +121,9 @@ func TestMemoryList(t *testing.T) { } // list all superseded releases - ssd, err := ts.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := ts.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -128,8 +134,9 @@ func TestMemoryList(t *testing.T) { } // list all deleted releases - del, err := ts.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := ts.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -185,25 +192,25 @@ func TestMemoryUpdate(t *testing.T) { { "update release status", "rls-a.v4", - releaseStub("rls-a", 4, "default", rspb.StatusSuperseded), + releaseStub("rls-a", 4, "default", common.StatusSuperseded), false, }, { "update release does not exist", "rls-c.v1", - releaseStub("rls-c", 1, "default", rspb.StatusUninstalled), + releaseStub("rls-c", 1, "default", common.StatusUninstalled), true, }, { "update release status in namespace", "rls-c.v4", - releaseStub("rls-c", 4, "mynamespace", rspb.StatusSuperseded), + releaseStub("rls-c", 4, "mynamespace", common.StatusSuperseded), false, }, { "update release in namespace does not exist", "rls-a.v1", - releaseStub("rls-a", 1, "mynamespace", rspb.StatusUninstalled), + releaseStub("rls-a", 1, "mynamespace", common.StatusUninstalled), true, }, } @@ -255,17 +262,23 @@ func TestMemoryDelete(t *testing.T) { startLen := len(start) for _, tt := range tests { ts.SetNamespace(tt.namespace) - if rel, err := ts.Delete(tt.key); err != nil { + + rel, err := ts.Delete(tt.key) + var rls *rspb.Release + if err == nil { + rls = convertReleaserToV1(t, rel) + } + if err != nil { if !tt.err { t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) } continue } else if tt.err { t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key) - } else if fmt.Sprintf("%s.v%d", rel.Name, rel.Version) != tt.key { - t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rel.Version) + } else if fmt.Sprintf("%s.v%d", rls.Name, rls.Version) != tt.key { + t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rls.Version) } - _, err := ts.Get(tt.key) + _, err = ts.Get(tt.key) if err == nil { t.Errorf("Expected an error when asking for a deleted key") } @@ -282,7 +295,9 @@ func TestMemoryDelete(t *testing.T) { if startLen-2 != endLen { t.Errorf("expected end to be %d instead of %d", startLen-2, endLen) for _, ee := range end { - t.Logf("Name: %s, Version: %d", ee.Name, ee.Version) + rac, err := release.NewAccessor(ee) + assert.NoError(t, err, "unable to get release accessor") + t.Logf("Name: %s, Version: %d", rac.Name(), rac.Version()) } } diff --git a/pkg/helm/pkg/storage/driver/mock_test.go b/pkg/helm/pkg/storage/driver/mock_test.go index 0061f021..e6290256 100644 --- a/pkg/helm/pkg/storage/driver/mock_test.go +++ b/pkg/helm/pkg/storage/driver/mock_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "context" @@ -31,10 +31,11 @@ import ( kblabels "k8s.io/apimachinery/pkg/labels" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) -func releaseStub(name string, vers int, namespace string, status rspb.Status) *rspb.Release { +func releaseStub(name string, vers int, namespace string, status common.Status) *rspb.Release { return &rspb.Release{ Name: name, Version: vers, @@ -52,22 +53,23 @@ func testKey(name string, vers int) string { } func tsFixtureMemory(t *testing.T) *Memory { + t.Helper() hs := []*rspb.Release{ // rls-a - releaseStub("rls-a", 4, "default", rspb.StatusDeployed), - releaseStub("rls-a", 1, "default", rspb.StatusSuperseded), - releaseStub("rls-a", 3, "default", rspb.StatusSuperseded), - releaseStub("rls-a", 2, "default", rspb.StatusSuperseded), + releaseStub("rls-a", 4, "default", common.StatusDeployed), + releaseStub("rls-a", 1, "default", common.StatusSuperseded), + releaseStub("rls-a", 3, "default", common.StatusSuperseded), + releaseStub("rls-a", 2, "default", common.StatusSuperseded), // rls-b - releaseStub("rls-b", 4, "default", rspb.StatusDeployed), - releaseStub("rls-b", 1, "default", rspb.StatusSuperseded), - releaseStub("rls-b", 3, "default", rspb.StatusSuperseded), - releaseStub("rls-b", 2, "default", rspb.StatusSuperseded), + releaseStub("rls-b", 4, "default", common.StatusDeployed), + releaseStub("rls-b", 1, "default", common.StatusSuperseded), + releaseStub("rls-b", 3, "default", common.StatusSuperseded), + releaseStub("rls-b", 2, "default", common.StatusSuperseded), // rls-c in other namespace - releaseStub("rls-c", 4, "mynamespace", rspb.StatusDeployed), - releaseStub("rls-c", 1, "mynamespace", rspb.StatusSuperseded), - releaseStub("rls-c", 3, "mynamespace", rspb.StatusSuperseded), - releaseStub("rls-c", 2, "mynamespace", rspb.StatusSuperseded), + releaseStub("rls-c", 4, "mynamespace", common.StatusDeployed), + releaseStub("rls-c", 1, "mynamespace", common.StatusSuperseded), + releaseStub("rls-c", 3, "mynamespace", common.StatusSuperseded), + releaseStub("rls-c", 2, "mynamespace", common.StatusSuperseded), } mem := NewMemory() @@ -80,9 +82,10 @@ func tsFixtureMemory(t *testing.T) *Memory { return mem } -// newTestFixture initializes a MockConfigMapsInterface. +// newTestFixtureCfgMaps initializes a MockConfigMapsInterface. // ConfigMaps are created for each release provided. func newTestFixtureCfgMaps(t *testing.T, releases ...*rspb.Release) *ConfigMaps { + t.Helper() var mock MockConfigMapsInterface mock.Init(t, releases...) @@ -98,6 +101,7 @@ type MockConfigMapsInterface struct { // Init initializes the MockConfigMapsInterface with the set of releases. func (mock *MockConfigMapsInterface) Init(t *testing.T, releases ...*rspb.Release) { + t.Helper() mock.objects = map[string]*v1.ConfigMap{} for _, rls := range releases { @@ -120,7 +124,7 @@ func (mock *MockConfigMapsInterface) Get(_ context.Context, name string, _ metav return object, nil } -// List returns the a of ConfigMaps. +// List returns all ConfigMaps. func (mock *MockConfigMapsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.ConfigMapList, error) { var list v1.ConfigMapList @@ -130,7 +134,7 @@ func (mock *MockConfigMapsInterface) List(_ context.Context, opts metav1.ListOpt } for _, cfgmap := range mock.objects { - if labelSelector.Matches(kblabels.Set(cfgmap.ObjectMeta.Labels)) { + if labelSelector.Matches(kblabels.Set(cfgmap.Labels)) { list.Items = append(list.Items, *cfgmap) } } @@ -139,7 +143,7 @@ func (mock *MockConfigMapsInterface) List(_ context.Context, opts metav1.ListOpt // Create creates a new ConfigMap. func (mock *MockConfigMapsInterface) Create(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.CreateOptions) (*v1.ConfigMap, error) { - name := cfgmap.ObjectMeta.Name + name := cfgmap.Name if object, ok := mock.objects[name]; ok { return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name) } @@ -149,7 +153,7 @@ func (mock *MockConfigMapsInterface) Create(_ context.Context, cfgmap *v1.Config // Update updates a ConfigMap. func (mock *MockConfigMapsInterface) Update(_ context.Context, cfgmap *v1.ConfigMap, _ metav1.UpdateOptions) (*v1.ConfigMap, error) { - name := cfgmap.ObjectMeta.Name + name := cfgmap.Name if _, ok := mock.objects[name]; !ok { return nil, apierrors.NewNotFound(v1.Resource("tests"), name) } @@ -166,9 +170,10 @@ func (mock *MockConfigMapsInterface) Delete(_ context.Context, name string, _ me return nil } -// newTestFixture initializes a MockSecretsInterface. +// newTestFixtureSecrets initializes a MockSecretsInterface. // Secrets are created for each release provided. func newTestFixtureSecrets(t *testing.T, releases ...*rspb.Release) *Secrets { + t.Helper() var mock MockSecretsInterface mock.Init(t, releases...) @@ -184,6 +189,7 @@ type MockSecretsInterface struct { // Init initializes the MockSecretsInterface with the set of releases. func (mock *MockSecretsInterface) Init(t *testing.T, releases ...*rspb.Release) { + t.Helper() mock.objects = map[string]*v1.Secret{} for _, rls := range releases { @@ -206,7 +212,7 @@ func (mock *MockSecretsInterface) Get(_ context.Context, name string, _ metav1.G return object, nil } -// List returns the a of Secret. +// List returns all Secrets. func (mock *MockSecretsInterface) List(_ context.Context, opts metav1.ListOptions) (*v1.SecretList, error) { var list v1.SecretList @@ -216,7 +222,7 @@ func (mock *MockSecretsInterface) List(_ context.Context, opts metav1.ListOption } for _, secret := range mock.objects { - if labelSelector.Matches(kblabels.Set(secret.ObjectMeta.Labels)) { + if labelSelector.Matches(kblabels.Set(secret.Labels)) { list.Items = append(list.Items, *secret) } } @@ -225,7 +231,7 @@ func (mock *MockSecretsInterface) List(_ context.Context, opts metav1.ListOption // Create creates a new Secret. func (mock *MockSecretsInterface) Create(_ context.Context, secret *v1.Secret, _ metav1.CreateOptions) (*v1.Secret, error) { - name := secret.ObjectMeta.Name + name := secret.Name if object, ok := mock.objects[name]; ok { return object, apierrors.NewAlreadyExists(v1.Resource("tests"), name) } @@ -235,7 +241,7 @@ func (mock *MockSecretsInterface) Create(_ context.Context, secret *v1.Secret, _ // Update updates a Secret. func (mock *MockSecretsInterface) Update(_ context.Context, secret *v1.Secret, _ metav1.UpdateOptions) (*v1.Secret, error) { - name := secret.ObjectMeta.Name + name := secret.Name if _, ok := mock.objects[name]; !ok { return nil, apierrors.NewNotFound(v1.Resource("tests"), name) } @@ -254,6 +260,7 @@ func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav // newTestFixtureSQL mocks the SQL database (for testing purposes) func newTestFixtureSQL(t *testing.T, _ ...*rspb.Release) (*SQL, sqlmock.Sqlmock) { + t.Helper() sqlDB, mock, err := sqlmock.New() if err != nil { t.Fatalf("error when opening stub database connection: %v", err) @@ -262,7 +269,6 @@ func newTestFixtureSQL(t *testing.T, _ ...*rspb.Release) (*SQL, sqlmock.Sqlmock) sqlxDB := sqlx.NewDb(sqlDB, "sqlmock") return &SQL{ db: sqlxDB, - Log: func(a string, b ...interface{}) {}, namespace: "default", statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), }, mock diff --git a/pkg/helm/pkg/storage/driver/records.go b/pkg/helm/pkg/storage/driver/records.go index 284667c4..00bf63a5 100644 --- a/pkg/helm/pkg/storage/driver/records.go +++ b/pkg/helm/pkg/storage/driver/records.go @@ -14,13 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "sort" "strconv" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) // records holds a list of in-memory release records diff --git a/pkg/helm/pkg/storage/driver/records_test.go b/pkg/helm/pkg/storage/driver/records_test.go index 7bf0bb2f..61a6c7be 100644 --- a/pkg/helm/pkg/storage/driver/records_test.go +++ b/pkg/helm/pkg/storage/driver/records_test.go @@ -14,19 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "reflect" "testing" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" ) func TestRecordsAdd(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -39,13 +39,13 @@ func TestRecordsAdd(t *testing.T) { "add valid key", "rls-a.v3", false, - newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)), }, { "add already existing key", "rls-a.v1", true, - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusDeployed)), }, } @@ -70,8 +70,8 @@ func TestRecordsRemove(t *testing.T) { } rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) startLen := rs.Len() @@ -98,8 +98,8 @@ func TestRecordsRemove(t *testing.T) { func TestRecordsRemoveAt(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) if len(rs) != 2 { @@ -114,8 +114,8 @@ func TestRecordsRemoveAt(t *testing.T) { func TestRecordsGet(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -126,7 +126,7 @@ func TestRecordsGet(t *testing.T) { { "get valid key", "rls-a.v1", - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), }, { "get invalid key", @@ -145,8 +145,8 @@ func TestRecordsGet(t *testing.T) { func TestRecordsIndex(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -176,8 +176,8 @@ func TestRecordsIndex(t *testing.T) { func TestRecordsExists(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -207,8 +207,8 @@ func TestRecordsExists(t *testing.T) { func TestRecordsReplace(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -220,13 +220,13 @@ func TestRecordsReplace(t *testing.T) { { "replace with existing key", "rls-a.v2", - newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }, { "replace with non existing key", "rls-a.v4", - newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", rspb.StatusDeployed)), + newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", common.StatusDeployed)), nil, }, } diff --git a/pkg/helm/pkg/storage/driver/secrets.go b/pkg/helm/pkg/storage/driver/secrets.go index e180a400..a33c0284 100644 --- a/pkg/helm/pkg/storage/driver/secrets.go +++ b/pkg/helm/pkg/storage/driver/secrets.go @@ -14,15 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "context" + "fmt" + "log/slog" "strconv" "strings" "time" - "github.com/pkg/errors" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,7 +31,9 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) var _ Driver = (*Secrets)(nil) @@ -42,16 +45,18 @@ const SecretsDriverName = "Secret" // SecretsInterface. type Secrets struct { impl corev1.SecretInterface - Log func(string, ...interface{}) + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // NewSecrets initializes a new Secrets wrapping an implementation of // the kubernetes SecretsInterface. func NewSecrets(impl corev1.SecretInterface) *Secrets { - return &Secrets{ + s := &Secrets{ impl: impl, - Log: func(_ string, _ ...interface{}) {}, } + s.SetLogger(slog.Default().Handler()) + return s } // Name returns the name of the driver. @@ -61,45 +66,51 @@ func (secrets *Secrets) Name() string { // Get fetches the release named by key. The corresponding release is returned // or error if not found. -func (secrets *Secrets) Get(key string) (*rspb.Release, error) { +func (secrets *Secrets) Get(key string) (release.Releaser, error) { // fetch the secret holding the release named by key obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { if apierrors.IsNotFound(err) { return nil, ErrReleaseNotFound } - return nil, errors.Wrapf(err, "get: failed to get %q", key) + return nil, fmt.Errorf("get: failed to get %q: %w", key, err) } // found the secret, decode the base64 data string r, err := decodeRelease(string(obj.Data["release"])) - r.Labels = filterSystemLabels(obj.ObjectMeta.Labels) - return r, errors.Wrapf(err, "get: failed to decode data %q", key) + if err != nil { + return r, fmt.Errorf("get: failed to decode data %q: %w", key, err) + } + r.Labels = filterSystemLabels(obj.Labels) + return r, nil } // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // secret fails to retrieve the releases. -func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (secrets *Secrets) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} list, err := secrets.impl.List(context.Background(), opts) if err != nil { - return nil, errors.Wrap(err, "list: failed to list") + return nil, fmt.Errorf("list: failed to list: %w", err) } - var results []*rspb.Release + var results []release.Releaser // iterate over the secrets object list // and decode each release for _, item := range list.Items { rls, err := decodeRelease(string(item.Data["release"])) if err != nil { - secrets.Log("list: failed to decode release: %v: %s", item, err) + secrets.Logger().Debug( + "list failed to decode release", slog.String("key", item.Name), + slog.Any("error", err), + ) continue } - rls.Labels = item.ObjectMeta.Labels + rls.Labels = item.Labels if filter(rls) { results = append(results, rls) @@ -110,11 +121,11 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, // Query fetches all releases that match the provided map of labels. // An error is returned if the secret fails to retrieve the releases. -func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) { +func (secrets *Secrets) Query(labels map[string]string) ([]release.Releaser, error) { ls := kblabels.Set{} for k, v := range labels { if errs := validation.IsValidLabelValue(v); len(errs) != 0 { - return nil, errors.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) + return nil, fmt.Errorf("invalid label value: %q: %s", v, strings.Join(errs, "; ")) } ls[k] = v } @@ -123,21 +134,25 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) list, err := secrets.impl.List(context.Background(), opts) if err != nil { - return nil, errors.Wrap(err, "query: failed to query with labels") + return nil, fmt.Errorf("query: failed to query with labels: %w", err) } if len(list.Items) == 0 { return nil, ErrReleaseNotFound } - var results []*rspb.Release + var results []release.Releaser for _, item := range list.Items { rls, err := decodeRelease(string(item.Data["release"])) if err != nil { - secrets.Log("query: failed to decode release: %s", err) + secrets.Logger().Debug( + "failed to decode release", + slog.String("key", item.Name), + slog.Any("error", err), + ) continue } - rls.Labels = item.ObjectMeta.Labels + rls.Labels = item.Labels results = append(results, rls) } return results, nil @@ -145,18 +160,23 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) // Create creates a new Secret holding the release. If the // Secret already exists, ErrReleaseExists is returned. -func (secrets *Secrets) Create(key string, rls *rspb.Release) error { +func (secrets *Secrets) Create(key string, rel release.Releaser) error { // set labels for secrets object meta data var lbs labels + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + lbs.init() lbs.fromMap(rls.Labels) - lbs.set("createdAt", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix())) // create a new secret to hold the release obj, err := newSecretsObject(key, rls, lbs) if err != nil { - return errors.Wrapf(err, "create: failed to encode release %q", rls.Name) + return fmt.Errorf("create: failed to encode release %q: %w", rls.Name, err) } // push the secret object out into the kubiverse if _, err := secrets.impl.Create(context.Background(), obj, metav1.CreateOptions{}); err != nil { @@ -164,40 +184,51 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error { return ErrReleaseExists } - return errors.Wrap(err, "create: failed to create") + return fmt.Errorf("create: failed to create: %w", err) } return nil } // Update updates the Secret holding the release. If not found // the Secret is created to hold the release. -func (secrets *Secrets) Update(key string, rls *rspb.Release) error { +func (secrets *Secrets) Update(key string, rel release.Releaser) error { // set labels for secrets object meta data var lbs labels + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + lbs.init() lbs.fromMap(rls.Labels) - lbs.set("modifiedAt", strconv.Itoa(int(time.Now().Unix()))) + lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix())) // create a new secret object to hold the release obj, err := newSecretsObject(key, rls, lbs) if err != nil { - return errors.Wrapf(err, "update: failed to encode release %q", rls.Name) + return fmt.Errorf("update: failed to encode release %q: %w", rls.Name, err) } // push the secret object out into the kubiverse _, err = secrets.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) - return errors.Wrap(err, "update: failed to update") + if err != nil { + return fmt.Errorf("update: failed to update: %w", err) + } + return nil } // Delete deletes the Secret holding the release named by key. -func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) { +func (secrets *Secrets) Delete(key string) (rls release.Releaser, err error) { // fetch the release to check existence if rls, err = secrets.Get(key); err != nil { return nil, err } // delete the release err = secrets.impl.Delete(context.Background(), key, metav1.DeleteOptions{}) - return rls, err + if err != nil { + return nil, err + } + return rls, nil } // newSecretsObject constructs a kubernetes Secret object diff --git a/pkg/helm/pkg/storage/driver/secrets_test.go b/pkg/helm/pkg/storage/driver/secrets_test.go index e694daaa..090c23f0 100644 --- a/pkg/helm/pkg/storage/driver/secrets_test.go +++ b/pkg/helm/pkg/storage/driver/secrets_test.go @@ -16,12 +16,15 @@ package driver import ( "encoding/base64" "encoding/json" + "errors" "reflect" "testing" v1 "k8s.io/api/core/v1" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) func TestSecretName(t *testing.T) { @@ -36,7 +39,7 @@ func TestSecretGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) @@ -56,7 +59,7 @@ func TestUNcompressedSecretGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // Create a test fixture which contains an uncompressed release secret, err := newSecretsObject(key, rel, nil) @@ -85,17 +88,18 @@ func TestUNcompressedSecretGet(t *testing.T) { func TestSecretList(t *testing.T) { secrets := newTestFixtureSecrets(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) // list all deleted releases - del, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := secrets.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -106,8 +110,9 @@ func TestSecretList(t *testing.T) { } // list all deployed releases - dpl, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := secrets.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -118,8 +123,9 @@ func TestSecretList(t *testing.T) { } // list all superseded releases - ssd, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := secrets.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -129,7 +135,7 @@ func TestSecretList(t *testing.T) { t.Errorf("Expected 2 superseded, got %d", len(ssd)) } // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. - rls := ssd[0] + rls := convertReleaserToV1(t, ssd[0]) _, ok := rls.Labels["name"] if !ok { t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) @@ -142,12 +148,12 @@ func TestSecretList(t *testing.T) { func TestSecretQuery(t *testing.T) { secrets := newTestFixtureSecrets(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) rls, err := secrets.Query(map[string]string{"status": "deployed"}) @@ -171,7 +177,7 @@ func TestSecretCreate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // store the release in a secret if err := secrets.Create(key, rel); err != nil { @@ -195,12 +201,12 @@ func TestSecretUpdate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) // modify release status code - rel.Info.Status = rspb.StatusSuperseded + rel.Info.Status = common.StatusSuperseded // perform the update if err := secrets.Update(key, rel); err != nil { @@ -208,10 +214,11 @@ func TestSecretUpdate(t *testing.T) { } // fetch the updated release - got, err := secrets.Get(key) + goti, err := secrets.Get(key) if err != nil { t.Fatalf("Failed to get release with key %q: %s", key, err) } + got := convertReleaserToV1(t, goti) // check release has actually been updated by comparing modified fields if rel.Info.Status != got.Info.Status { @@ -224,7 +231,7 @@ func TestSecretDelete(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) @@ -242,10 +249,8 @@ func TestSecretDelete(t *testing.T) { if !reflect.DeepEqual(rel, rls) { t.Errorf("Expected {%v}, got {%v}", rel, rls) } - - // fetch the deleted release _, err = secrets.Get(key) - if !reflect.DeepEqual(ErrReleaseNotFound, err) { + if !errors.Is(err, ErrReleaseNotFound) { t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) } } diff --git a/pkg/helm/pkg/storage/driver/sql.go b/pkg/helm/pkg/storage/driver/sql.go index 8a41cef9..5e73a756 100644 --- a/pkg/helm/pkg/storage/driver/sql.go +++ b/pkg/helm/pkg/storage/driver/sql.go @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "fmt" + "log/slog" + "maps" "sort" "strconv" "time" @@ -30,7 +32,9 @@ import ( // Import pq for postgres dialect _ "github.com/lib/pq" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) var _ Driver = (*SQL)(nil) @@ -72,8 +76,8 @@ const ( // Following limits based on k8s labels limits - https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#syntax-and-character-set const ( - sqlCustomLabelsTableKeyMaxLenght = 253 + 1 + 63 - sqlCustomLabelsTableValueMaxLenght = 63 + sqlCustomLabelsTableKeyMaxLength = 253 + 1 + 63 + sqlCustomLabelsTableValueMaxLength = 63 ) const ( @@ -86,8 +90,8 @@ type SQL struct { db *sqlx.DB namespace string statementBuilder sq.StatementBuilderType - - Log func(string, ...interface{}) + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // Name returns the name of the driver. @@ -98,9 +102,9 @@ func (s *SQL) Name() string { // Check if all migrations al func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool { // make map (set) of ids for fast search - migrationsIds := make(map[string]struct{}) + migrationsIDs := make(map[string]struct{}) for _, migration := range migrations { - migrationsIds[migration.Id] = struct{}{} + migrationsIDs[migration.Id] = struct{}{} } // get list of applied migrations @@ -108,21 +112,21 @@ func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool { records, err := migrate.GetMigrationRecords(s.db.DB, postgreSQLDialect) migrate.SetDisableCreateTable(false) if err != nil { - s.Log("checkAlreadyApplied: failed to get migration records: %v", err) + s.Logger().Debug("failed to get migration records", slog.Any("error", err)) return false } for _, record := range records { - if _, ok := migrationsIds[record.Id]; ok { - s.Log("checkAlreadyApplied: found previous migration (Id: %v) applied at %v", record.Id, record.AppliedAt) - delete(migrationsIds, record.Id) + if _, ok := migrationsIDs[record.Id]; ok { + s.Logger().Debug("found previous migration", "id", record.Id, "appliedAt", record.AppliedAt) + delete(migrationsIDs, record.Id) } } - // check if all migrations appliyed - if len(migrationsIds) != 0 { - for id := range migrationsIds { - s.Log("checkAlreadyApplied: find unapplied migration (id: %v)", id) + // check if all migrations applied + if len(migrationsIDs) != 0 { + for id := range migrationsIDs { + s.Logger().Debug("find unapplied migration", "id", id) } return false } @@ -156,9 +160,9 @@ func (s *SQL) ensureDBSetup() error { CREATE INDEX ON %s (%s); CREATE INDEX ON %s (%s); CREATE INDEX ON %s (%s); - + GRANT ALL ON %s TO PUBLIC; - + ALTER TABLE %s ENABLE ROW LEVEL SECURITY; `, sqlReleaseTableName, @@ -204,11 +208,11 @@ func (s *SQL) ensureDBSetup() error { CREATE TABLE %s ( %s VARCHAR(64), %s VARCHAR(67), - %s VARCHAR(%d), + %s VARCHAR(%d), %s VARCHAR(%d) ); CREATE INDEX ON %s (%s, %s); - + GRANT ALL ON %s TO PUBLIC; ALTER TABLE %s ENABLE ROW LEVEL SECURITY; `, @@ -216,9 +220,9 @@ func (s *SQL) ensureDBSetup() error { sqlCustomLabelsTableReleaseKeyColumn, sqlCustomLabelsTableReleaseNamespaceColumn, sqlCustomLabelsTableKeyColumn, - sqlCustomLabelsTableKeyMaxLenght, + sqlCustomLabelsTableKeyMaxLength, sqlCustomLabelsTableValueColumn, - sqlCustomLabelsTableValueMaxLenght, + sqlCustomLabelsTableValueMaxLength, sqlCustomLabelsTableName, sqlCustomLabelsTableReleaseKeyColumn, sqlCustomLabelsTableReleaseNamespaceColumn, @@ -276,7 +280,7 @@ type SQLReleaseCustomLabelWrapper struct { } // NewSQL initializes a new sql driver. -func NewSQL(connectionString string, logger func(string, ...interface{}), namespace string) (*SQL, error) { +func NewSQL(connectionString string, namespace string) (*SQL, error) { db, err := sqlx.Connect(postgreSQLDialect, connectionString) if err != nil { return nil, err @@ -284,7 +288,6 @@ func NewSQL(connectionString string, logger func(string, ...interface{}), namesp driver := &SQL{ db: db, - Log: logger, statementBuilder: sq.StatementBuilder.PlaceholderFormat(sq.Dollar), } @@ -293,12 +296,13 @@ func NewSQL(connectionString string, logger func(string, ...interface{}), namesp } driver.namespace = namespace + driver.SetLogger(slog.Default().Handler()) return driver, nil } // Get returns the release named by key. -func (s *SQL) Get(key string) (*rspb.Release, error) { +func (s *SQL) Get(key string) (release.Releaser, error) { var record SQLReleaseWrapper qb := s.statementBuilder. @@ -309,24 +313,29 @@ func (s *SQL) Get(key string) (*rspb.Release, error) { query, args, err := qb.ToSql() if err != nil { - s.Log("failed to build query: %v", err) + s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } // Get will return an error if the result is empty if err := s.db.Get(&record, query, args...); err != nil { - s.Log("got SQL error when getting release %s: %v", key, err) + s.Logger().Debug("got SQL error when getting release", slog.String("key", key), slog.Any("error", err)) return nil, ErrReleaseNotFound } release, err := decodeRelease(record.Body) if err != nil { - s.Log("get: failed to decode data %q: %v", key, err) + s.Logger().Debug("failed to decode data", slog.String("key", key), slog.Any("error", err)) return nil, err } if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil { - s.Log("failed to get release %s/%s custom labels: %v", s.namespace, key, err) + s.Logger().Debug( + "failed to get release custom labels", + slog.String("namespace", s.namespace), + slog.String("key", key), + slog.Any("error", err), + ) return nil, err } @@ -334,7 +343,7 @@ func (s *SQL) Get(key string) (*rspb.Release, error) { } // List returns the list of all releases such that filter(release) == true -func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (s *SQL) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { sb := s.statementBuilder. Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName). @@ -347,31 +356,34 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { query, args, err := sb.ToSql() if err != nil { - s.Log("failed to build query: %v", err) + s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } var records = []SQLReleaseWrapper{} if err := s.db.Select(&records, query, args...); err != nil { - s.Log("list: failed to list: %v", err) + s.Logger().Debug("failed to list", slog.Any("error", err)) return nil, err } - var releases []*rspb.Release + var releases []release.Releaser for _, record := range records { release, err := decodeRelease(record.Body) if err != nil { - s.Log("list: failed to decode release: %v: %v", record, err) + s.Logger().Debug("failed to decode release", slog.Any("record", record), slog.Any("error", err)) continue } if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil { - s.Log("failed to get release %s/%s custom labels: %v", record.Namespace, record.Key, err) + s.Logger().Debug( + "failed to get release custom labels", + slog.String("namespace", record.Namespace), + slog.String("key", record.Key), + slog.Any("error", err), + ) return nil, err } - for k, v := range getReleaseSystemLabels(release) { - release.Labels[k] = v - } + maps.Copy(release.Labels, getReleaseSystemLabels(release)) if filter(release) { releases = append(releases, release) @@ -382,7 +394,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { } // Query returns the set of releases that match the provided set of labels. -func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { +func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) { sb := s.statementBuilder. Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName) @@ -396,7 +408,7 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { if _, ok := labelMap[key]; ok { sb = sb.Where(sq.Eq{key: labels[key]}) } else { - s.Log("unknown label %s", key) + s.Logger().Debug("unknown label", "key", key) return nil, fmt.Errorf("unknown label %s", key) } } @@ -409,13 +421,13 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { // Build our query query, args, err := sb.ToSql() if err != nil { - s.Log("failed to build query: %v", err) + s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } var records = []SQLReleaseWrapper{} if err := s.db.Select(&records, query, args...); err != nil { - s.Log("list: failed to query with labels: %v", err) + s.Logger().Debug("failed to query with labels", slog.Any("error", err)) return nil, err } @@ -423,16 +435,21 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { return nil, ErrReleaseNotFound } - var releases []*rspb.Release + var releases []release.Releaser for _, record := range records { release, err := decodeRelease(record.Body) if err != nil { - s.Log("list: failed to decode release: %v: %v", record, err) + s.Logger().Debug("failed to decode release", slog.Any("record", record), slog.Any("error", err)) continue } if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil { - s.Log("failed to get release %s/%s custom labels: %v", record.Namespace, record.Key, err) + s.Logger().Debug( + "failed to get release custom labels", + slog.String("namespace", record.Namespace), + slog.String("key", record.Key), + slog.Any("error", err), + ) return nil, err } @@ -447,7 +464,12 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { } // Create creates a new release. -func (s *SQL) Create(key string, rls *rspb.Release) error { +func (s *SQL) Create(key string, rel release.Releaser) error { + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + namespace := rls.Namespace if namespace == "" { namespace = defaultNamespace @@ -456,13 +478,13 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { body, err := encodeRelease(rls) if err != nil { - s.Log("failed to encode release: %v", err) + s.Logger().Debug("failed to encode release", slog.Any("error", err)) return err } transaction, err := s.db.Beginx() if err != nil { - s.Log("failed to start SQL transaction: %v", err) + s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err)) return fmt.Errorf("error beginning transaction: %v", err) } @@ -491,7 +513,7 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { int(time.Now().Unix()), ).ToSql() if err != nil { - s.Log("failed to build insert query: %v", err) + s.Logger().Debug("failed to build insert query", slog.Any("error", err)) return err } @@ -505,17 +527,17 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if buildErr != nil { - s.Log("failed to build select query: %v", buildErr) + s.Logger().Debug("failed to build select query", "error", buildErr) return err } var record SQLReleaseWrapper if err := transaction.Get(&record, selectQuery, args...); err == nil { - s.Log("release %s already exists", key) + s.Logger().Debug("release already exists", slog.String("key", key)) return ErrReleaseExists } - s.Log("failed to store release %s in SQL database: %v", key, err) + s.Logger().Debug("failed to store release in SQL database", slog.String("key", key), slog.Any("error", err)) return err } @@ -538,13 +560,13 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { if err != nil { defer transaction.Rollback() - s.Log("failed to build insert query: %v", err) + s.Logger().Debug("failed to build insert query", slog.Any("error", err)) return err } if _, err := transaction.Exec(insertLabelsQuery, args...); err != nil { defer transaction.Rollback() - s.Log("failed to write Labels: %v", err) + s.Logger().Debug("failed to write Labels", slog.Any("error", err)) return err } } @@ -554,7 +576,11 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { } // Update updates a release. -func (s *SQL) Update(key string, rls *rspb.Release) error { +func (s *SQL) Update(key string, rel release.Releaser) error { + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } namespace := rls.Namespace if namespace == "" { namespace = defaultNamespace @@ -563,7 +589,7 @@ func (s *SQL) Update(key string, rls *rspb.Release) error { body, err := encodeRelease(rls) if err != nil { - s.Log("failed to encode release: %v", err) + s.Logger().Debug("failed to encode release", slog.Any("error", err)) return err } @@ -580,12 +606,12 @@ func (s *SQL) Update(key string, rls *rspb.Release) error { ToSql() if err != nil { - s.Log("failed to build update query: %v", err) + s.Logger().Debug("failed to build update query", slog.Any("error", err)) return err } if _, err := s.db.Exec(query, args...); err != nil { - s.Log("failed to update release %s in SQL database: %v", key, err) + s.Logger().Debug("failed to update release in SQL database", slog.String("key", key), slog.Any("error", err)) return err } @@ -593,10 +619,10 @@ func (s *SQL) Update(key string, rls *rspb.Release) error { } // Delete deletes a release or returns ErrReleaseNotFound. -func (s *SQL) Delete(key string) (*rspb.Release, error) { +func (s *SQL) Delete(key string) (release.Releaser, error) { transaction, err := s.db.Beginx() if err != nil { - s.Log("failed to start SQL transaction: %v", err) + s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err)) return nil, fmt.Errorf("error beginning transaction: %v", err) } @@ -607,20 +633,20 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if err != nil { - s.Log("failed to build select query: %v", err) + s.Logger().Debug("failed to build select query", slog.Any("error", err)) return nil, err } var record SQLReleaseWrapper err = transaction.Get(&record, selectQuery, args...) if err != nil { - s.Log("release %s not found: %v", key, err) + s.Logger().Debug("release not found", slog.String("key", key), slog.Any("error", err)) return nil, ErrReleaseNotFound } release, err := decodeRelease(record.Body) if err != nil { - s.Log("failed to decode release %s: %v", key, err) + s.Logger().Debug("failed to decode release", slog.String("key", key), slog.Any("error", err)) transaction.Rollback() return nil, err } @@ -632,18 +658,22 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if err != nil { - s.Log("failed to build delete query: %v", err) + s.Logger().Debug("failed to build delete query", slog.Any("error", err)) return nil, err } _, err = transaction.Exec(deleteQuery, args...) if err != nil { - s.Log("failed perform delete query: %v", err) + s.Logger().Debug("failed perform delete query", slog.Any("error", err)) return release, err } if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil { - s.Log("failed to get release %s/%s custom labels: %v", s.namespace, key, err) + s.Logger().Debug( + "failed to get release custom labels", + slog.String("namespace", s.namespace), + slog.String("key", key), + slog.Any("error", err)) return nil, err } @@ -654,7 +684,7 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) { ToSql() if err != nil { - s.Log("failed to build delete Labels query: %v", err) + s.Logger().Debug("failed to build delete Labels query", slog.Any("error", err)) return nil, err } _, err = transaction.Exec(deleteCustomLabelsQuery, args...) diff --git a/pkg/helm/pkg/storage/driver/sql_test.go b/pkg/helm/pkg/storage/driver/sql_test.go index a1c1f131..a7d38b77 100644 --- a/pkg/helm/pkg/storage/driver/sql_test.go +++ b/pkg/helm/pkg/storage/driver/sql_test.go @@ -14,6 +14,7 @@ limitations under the License. package driver import ( + "database/sql/driver" "fmt" "reflect" "regexp" @@ -23,9 +24,38 @@ import ( sqlmock "github.com/DATA-DOG/go-sqlmock" migrate "github.com/rubenv/sql-migrate" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) +const recentTimestampTolerance = time.Second + +func recentUnixTimestamp() sqlmock.Argument { + return recentUnixTimestampArgument{} +} + +type recentUnixTimestampArgument struct{} + +func (recentUnixTimestampArgument) Match(value driver.Value) bool { + var ts int64 + switch v := value.(type) { + case int: + ts = int64(v) + case int64: + ts = v + default: + return false + } + + diff := time.Since(time.Unix(ts, 0)) + if diff < 0 { + diff = -diff + } + + return diff <= recentTimestampTolerance +} + func TestSQLName(t *testing.T) { sqlDriver, _ := newTestFixtureSQL(t) if sqlDriver.Name() != SQLDriverName { @@ -38,7 +68,7 @@ func TestSQLGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) body, _ := encodeRelease(rel) @@ -81,16 +111,16 @@ func TestSQLGet(t *testing.T) { func TestSQLList(t *testing.T) { releases := []*rspb.Release{} - releases = append(releases, releaseStub("key-1", 1, "default", rspb.StatusUninstalled)) - releases = append(releases, releaseStub("key-2", 1, "default", rspb.StatusUninstalled)) - releases = append(releases, releaseStub("key-3", 1, "default", rspb.StatusDeployed)) - releases = append(releases, releaseStub("key-4", 1, "default", rspb.StatusDeployed)) - releases = append(releases, releaseStub("key-5", 1, "default", rspb.StatusSuperseded)) - releases = append(releases, releaseStub("key-6", 1, "default", rspb.StatusSuperseded)) + releases = append(releases, releaseStub("key-1", 1, "default", common.StatusUninstalled)) + releases = append(releases, releaseStub("key-2", 1, "default", common.StatusUninstalled)) + releases = append(releases, releaseStub("key-3", 1, "default", common.StatusDeployed)) + releases = append(releases, releaseStub("key-4", 1, "default", common.StatusDeployed)) + releases = append(releases, releaseStub("key-5", 1, "default", common.StatusSuperseded)) + releases = append(releases, releaseStub("key-6", 1, "default", common.StatusSuperseded)) sqlDriver, mock := newTestFixtureSQL(t) - for i := 0; i < 3; i++ { + for range 3 { query := fmt.Sprintf( "SELECT %s, %s, %s FROM %s WHERE %s = $1 AND %s = $2", sqlReleaseTableKeyColumn, @@ -119,8 +149,9 @@ func TestSQLList(t *testing.T) { } // list all deleted releases - del, err := sqlDriver.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := sqlDriver.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -131,8 +162,9 @@ func TestSQLList(t *testing.T) { } // list all deployed releases - dpl, err := sqlDriver.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := sqlDriver.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -143,8 +175,9 @@ func TestSQLList(t *testing.T) { } // list all superseded releases - ssd, err := sqlDriver.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := sqlDriver.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -159,7 +192,7 @@ func TestSQLList(t *testing.T) { } // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. - rls := ssd[0] + rls := convertReleaserToV1(t, ssd[0]) _, ok := rls.Labels["name"] if !ok { t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) @@ -175,7 +208,7 @@ func TestSqlCreate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) sqlDriver, mock := newTestFixtureSQL(t) body, _ := encodeRelease(rel) @@ -197,7 +230,7 @@ func TestSqlCreate(t *testing.T) { mock.ExpectBegin() mock. ExpectExec(regexp.QuoteMeta(query)). - WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()). WillReturnResult(sqlmock.NewResult(1, 1)) labelsQuery := fmt.Sprintf( @@ -232,7 +265,7 @@ func TestSqlCreateAlreadyExists(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) sqlDriver, mock := newTestFixtureSQL(t) body, _ := encodeRelease(rel) @@ -255,7 +288,7 @@ func TestSqlCreateAlreadyExists(t *testing.T) { mock.ExpectBegin() mock. ExpectExec(regexp.QuoteMeta(insertQuery)). - WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()). WillReturnError(fmt.Errorf("dialect dependent SQL error")) selectQuery := fmt.Sprintf( @@ -293,7 +326,7 @@ func TestSqlUpdate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) sqlDriver, mock := newTestFixtureSQL(t) body, _ := encodeRelease(rel) @@ -313,7 +346,7 @@ func TestSqlUpdate(t *testing.T) { mock. ExpectExec(regexp.QuoteMeta(query)). - WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix()), key, namespace). + WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp(), key, namespace). WillReturnResult(sqlmock.NewResult(0, 1)) if err := sqlDriver.Update(key, rel); err != nil { @@ -342,9 +375,9 @@ func TestSqlQuery(t *testing.T) { "owner": sqlReleaseDefaultOwner, } - supersededRelease := releaseStub("smug-pigeon", 1, "default", rspb.StatusSuperseded) + supersededRelease := releaseStub("smug-pigeon", 1, "default", common.StatusSuperseded) supersededReleaseBody, _ := encodeRelease(supersededRelease) - deployedRelease := releaseStub("smug-pigeon", 2, "default", rspb.StatusDeployed) + deployedRelease := releaseStub("smug-pigeon", 2, "default", common.StatusDeployed) deployedReleaseBody, _ := encodeRelease(deployedRelease) // Let's actually start our test @@ -454,7 +487,7 @@ func TestSqlDelete(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) body, _ := encodeRelease(rel) @@ -543,34 +576,34 @@ func mockGetReleaseCustomLabels(mock sqlmock.Sqlmock, key string, namespace stri eq.WillReturnRows(returnRows).RowsWillBeClosed() } -func TestSqlChechkAppliedMigrations(t *testing.T) { +func TestSqlCheckAppliedMigrations(t *testing.T) { cases := []struct { migrationsToApply []*migrate.Migration - appliedMigrationsIds []string + appliedMigrationsIDs []string expectedResult bool errorExplanation string }{ { migrationsToApply: []*migrate.Migration{{Id: "init1"}, {Id: "init2"}, {Id: "init3"}}, - appliedMigrationsIds: []string{"1", "2", "init1", "3", "init2", "4", "5"}, + appliedMigrationsIDs: []string{"1", "2", "init1", "3", "init2", "4", "5"}, expectedResult: false, errorExplanation: "Has found one migration id \"init3\" as applied, that was not applied", }, { migrationsToApply: []*migrate.Migration{{Id: "init1"}, {Id: "init2"}, {Id: "init3"}}, - appliedMigrationsIds: []string{"1", "2", "init1", "3", "init2", "4", "init3", "5"}, + appliedMigrationsIDs: []string{"1", "2", "init1", "3", "init2", "4", "init3", "5"}, expectedResult: true, errorExplanation: "Has not found one or more migration ids, that was applied", }, { migrationsToApply: []*migrate.Migration{{Id: "init"}}, - appliedMigrationsIds: []string{"1", "2", "3", "inits", "4", "tinit", "5"}, + appliedMigrationsIDs: []string{"1", "2", "3", "inits", "4", "tinit", "5"}, expectedResult: false, errorExplanation: "Has found single \"init\", that was not applied", }, { migrationsToApply: []*migrate.Migration{{Id: "init"}}, - appliedMigrationsIds: []string{"1", "2", "init", "3", "init2", "4", "init3", "5"}, + appliedMigrationsIDs: []string{"1", "2", "init", "3", "init2", "4", "init3", "5"}, expectedResult: true, errorExplanation: "Has not found single migration id \"init\", that was applied", }, @@ -578,7 +611,7 @@ func TestSqlChechkAppliedMigrations(t *testing.T) { for i, c := range cases { sqlDriver, mock := newTestFixtureSQL(t) rows := sqlmock.NewRows([]string{"id", "applied_at"}) - for _, id := range c.appliedMigrationsIds { + for _, id := range c.appliedMigrationsIDs { rows.AddRow(id, time.Time{}) } mock. diff --git a/pkg/helm/pkg/storage/driver/util.go b/pkg/helm/pkg/storage/driver/util.go index 73335dea..c8a7bd15 100644 --- a/pkg/helm/pkg/storage/driver/util.go +++ b/pkg/helm/pkg/storage/driver/util.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package driver // import "helm.sh/helm/v3/pkg/storage/driver" +package driver // import "github.com/werf/nelm/pkg/helm/pkg/storage/driver" import ( "bytes" @@ -22,8 +22,9 @@ import ( "encoding/base64" "encoding/json" "io" + "slices" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) var b64 = base64.StdEncoding @@ -88,12 +89,7 @@ func decodeRelease(data string) (*rspb.Release, error) { // Checks if label is system func isSystemLabel(key string) bool { - for _, v := range GetSystemLabels() { - if key == v { - return true - } - } - return false + return slices.Contains(GetSystemLabels(), key) } // Removes system labels from labels map diff --git a/pkg/helm/pkg/storage/storage.go b/pkg/helm/pkg/storage/storage.go index 8cfe09e0..cb1ecb25 100644 --- a/pkg/helm/pkg/storage/storage.go +++ b/pkg/helm/pkg/storage/storage.go @@ -14,16 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package storage // import "helm.sh/helm/v3/pkg/storage" +package storage // import "github.com/werf/nelm/pkg/helm/pkg/storage" import ( + "errors" "fmt" + "log/slog" "strings" - "github.com/pkg/errors" - - rspb "github.com/werf/nelm/pkg/helm/pkg/release" - relutil "github.com/werf/nelm/pkg/helm/pkg/releaseutil" + "github.com/werf/nelm/pkg/helm/intern/logging" + "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" + relutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" "github.com/werf/nelm/pkg/helm/pkg/storage/driver" ) @@ -43,76 +46,114 @@ type Storage struct { // ignored (meaning no limits are imposed). MaxHistory int - Log func(string, ...interface{}) + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // Get retrieves the release from storage. An error is returned // if the storage driver failed to fetch the release, or the // release identified by the key, version pair does not exist. -func (s *Storage) Get(name string, version int) (*rspb.Release, error) { - s.Log("getting release %q", makeKey(name, version)) +func (s *Storage) Get(name string, version int) (release.Releaser, error) { + s.Logger().Debug("getting release", "key", makeKey(name, version)) return s.Driver.Get(makeKey(name, version)) } // Create creates a new storage entry holding the release. An // error is returned if the storage driver fails to store the // release, or a release with an identical key already exists. -func (s *Storage) Create(rls *rspb.Release) error { - s.Log("creating release %q", makeKey(rls.Name, rls.Version)) +func (s *Storage) Create(rls release.Releaser) error { + rac, err := release.NewAccessor(rls) + if err != nil { + return err + } + s.Logger().Debug("creating release", "key", makeKey(rac.Name(), rac.Version())) if s.MaxHistory > 0 { // Want to make space for one more release. - if err := s.removeLeastRecent(rls.Name, s.MaxHistory-1); err != nil && + if err := s.removeLeastRecent(rac.Name(), s.MaxHistory-1); err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { return err } } - return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) + return s.Driver.Create(makeKey(rac.Name(), rac.Version()), rls) } // Update updates the release in storage. An error is returned if the // storage backend fails to update the release or if the release // does not exist. -func (s *Storage) Update(rls *rspb.Release) error { - s.Log("updating release %q", makeKey(rls.Name, rls.Version)) - return s.Driver.Update(makeKey(rls.Name, rls.Version), rls) +func (s *Storage) Update(rls release.Releaser) error { + rac, err := release.NewAccessor(rls) + if err != nil { + return err + } + s.Logger().Debug("updating release", "key", makeKey(rac.Name(), rac.Version())) + return s.Driver.Update(makeKey(rac.Name(), rac.Version()), rls) } // Delete deletes the release from storage. An error is returned if // the storage backend fails to delete the release or if the release // does not exist. -func (s *Storage) Delete(name string, version int) (*rspb.Release, error) { - s.Log("deleting release %q", makeKey(name, version)) +func (s *Storage) Delete(name string, version int) (release.Releaser, error) { + s.Logger().Debug("deleting release", "key", makeKey(name, version)) return s.Driver.Delete(makeKey(name, version)) } // ListReleases returns all releases from storage. An error is returned if the // storage backend fails to retrieve the releases. -func (s *Storage) ListReleases() ([]*rspb.Release, error) { - s.Log("listing all releases in storage") - return s.Driver.List(func(_ *rspb.Release) bool { return true }) +func (s *Storage) ListReleases() ([]release.Releaser, error) { + s.Logger().Debug("listing all releases in storage") + return s.List(func(_ release.Releaser) bool { return true }) +} + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { + switch r := rel.(type) { + case rspb.Release: + return &r, nil + case *rspb.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } } // ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned // if the storage backend fails to retrieve the releases. -func (s *Storage) ListUninstalled() ([]*rspb.Release, error) { - s.Log("listing uninstalled releases in storage") - return s.Driver.List(func(rls *rspb.Release) bool { - return relutil.StatusFilter(rspb.StatusUninstalled).Check(rls) +func (s *Storage) ListUninstalled() ([]release.Releaser, error) { + s.Logger().Debug("listing uninstalled releases in storage") + return s.List(func(rls release.Releaser) bool { + rel, err := releaserToV1Release(rls) + if err != nil { + // This will only happen if calling code does not pass the proper types. This is + // a problem with the application and not user data. + s.Logger().Error("unable to convert release to typed release", slog.Any("error", err)) + panic(fmt.Sprintf("unable to convert release to typed release: %s", err)) + } + return relutil.StatusFilter(common.StatusUninstalled).Check(rel) }) } // ListDeployed returns all releases with Status == DEPLOYED. An error is returned // if the storage backend fails to retrieve the releases. -func (s *Storage) ListDeployed() ([]*rspb.Release, error) { - s.Log("listing all deployed releases in storage") - return s.Driver.List(func(rls *rspb.Release) bool { - return relutil.StatusFilter(rspb.StatusDeployed).Check(rls) +func (s *Storage) ListDeployed() ([]release.Releaser, error) { + s.Logger().Debug("listing all deployed releases in storage") + return s.List(func(rls release.Releaser) bool { + rel, err := releaserToV1Release(rls) + if err != nil { + // This will only happen if calling code does not pass the proper types. This is + // a problem with the application and not user data. + s.Logger().Error("unable to convert release to typed release", slog.Any("error", err)) + panic(fmt.Sprintf("unable to convert release to typed release: %s", err)) + } + return relutil.StatusFilter(common.StatusDeployed).Check(rel) }) } // Deployed returns the last deployed release with the provided release name, or -// returns ErrReleaseNotFound if not found. -func (s *Storage) Deployed(name string) (*rspb.Release, error) { +// returns driver.NewErrNoDeployedReleases if not found. +func (s *Storage) Deployed(name string) (release.Releaser, error) { ls, err := s.DeployedAll(name) if err != nil { return nil, err @@ -122,19 +163,37 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) { return nil, driver.NewErrNoDeployedReleases(name) } + rls, err := releaseListToV1List(ls) + if err != nil { + return nil, err + } + // If executed concurrently, Helm's database gets corrupted // and multiple releases are DEPLOYED. Take the latest. - relutil.Reverse(ls, relutil.SortByRevision) + relutil.Reverse(rls, relutil.SortByRevision) - return ls[0], nil + return rls[0], nil +} + +func releaseListToV1List(ls []release.Releaser) ([]*rspb.Release, error) { + rls := make([]*rspb.Release, 0, len(ls)) + for _, val := range ls { + rel, err := releaserToV1Release(val) + if err != nil { + return nil, err + } + rls = append(rls, rel) + } + + return rls, nil } // DeployedAll returns all deployed releases with the provided name, or -// returns ErrReleaseNotFound if not found. -func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { - s.Log("getting deployed releases from %q history", name) +// returns driver.NewErrNoDeployedReleases if not found. +func (s *Storage) DeployedAll(name string) ([]release.Releaser, error) { + s.Logger().Debug("getting deployed releases", "name", name) - ls, err := s.Driver.Query(map[string]string{ + ls, err := s.Query(map[string]string{ "name": name, "owner": "helm", "status": "deployed", @@ -149,11 +208,11 @@ func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { } // History returns the revision history for the release with the provided name, or -// returns ErrReleaseNotFound if no such release name exists. -func (s *Storage) History(name string) ([]*rspb.Release, error) { - s.Log("getting release history for %q", name) +// returns driver.ErrReleaseNotFound if no such release name exists. +func (s *Storage) History(name string) ([]release.Releaser, error) { + s.Logger().Debug("getting release history", "name", name) - return s.Driver.Query(map[string]string{"name": name, "owner": "helm"}) + return s.Query(map[string]string{"name": name, "owner": "helm"}) } // removeLeastRecent removes items from history until the length number of releases @@ -161,34 +220,42 @@ func (s *Storage) History(name string) ([]*rspb.Release, error) { // // We allow max to be set explicitly so that calling functions can "make space" // for the new records they are going to write. -func (s *Storage) removeLeastRecent(name string, max int) error { - if max < 0 { +func (s *Storage) removeLeastRecent(name string, maximum int) error { + if maximum < 0 { return nil } h, err := s.History(name) if err != nil { return err } - if len(h) <= max { + if len(h) <= maximum { return nil } + rls, err := releaseListToV1List(h) + if err != nil { + return err + } // We want oldest to newest - relutil.SortByRevision(h) + relutil.SortByRevision(rls) lastDeployed, err := s.Deployed(name) if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) { return err } - var toDelete []*rspb.Release - for _, rel := range h { - // once we have enough releases to delete to reach the max, stop - if len(h)-len(toDelete) == max { + var toDelete []release.Releaser + for _, rel := range rls { + // once we have enough releases to delete to reach the maximum, stop + if len(rls)-len(toDelete) == maximum { break } if lastDeployed != nil { - if rel.Version != lastDeployed.Version { + ldac, err := release.NewAccessor(lastDeployed) + if err != nil { + return err + } + if rel.Version != ldac.Version() { toDelete = append(toDelete, rel) } } else { @@ -200,20 +267,25 @@ func (s *Storage) removeLeastRecent(name string, max int) error { // multiple invocations of this function will eventually delete them all. errs := []error{} for _, rel := range toDelete { - err = s.deleteReleaseVersion(name, rel.Version) + rac, err := release.NewAccessor(rel) + if err != nil { + errs = append(errs, err) + continue + } + err = s.deleteReleaseVersion(name, rac.Version()) if err != nil { errs = append(errs, err) } } - s.Log("Pruned %d record(s) from %s with %d error(s)", len(toDelete), name, len(errs)) + s.Logger().Debug("pruned records", "count", len(toDelete), "release", name, "errors", len(errs)) switch c := len(errs); c { case 0: return nil case 1: return errs[0] default: - return errors.Errorf("encountered %d deletion errors. First is: %s", c, errs[0]) + return fmt.Errorf("encountered %d deletion errors. First is: %w", c, errs[0]) } } @@ -221,25 +293,29 @@ func (s *Storage) deleteReleaseVersion(name string, version int) error { key := makeKey(name, version) _, err := s.Delete(name, version) if err != nil { - s.Log("error pruning %s from release history: %s", key, err) + s.Logger().Debug("error pruning release", slog.String("key", key), slog.Any("error", err)) return err } return nil } // Last fetches the last revision of the named release. -func (s *Storage) Last(name string) (*rspb.Release, error) { - s.Log("getting last revision of %q", name) +func (s *Storage) Last(name string) (release.Releaser, error) { + s.Logger().Debug("getting last revision", "name", name) h, err := s.History(name) if err != nil { return nil, err } if len(h) == 0 { - return nil, errors.Errorf("no revision for release %q", name) + return nil, fmt.Errorf("no revision for release %q", name) + } + rls, err := releaseListToV1List(h) + if err != nil { + return nil, err } - relutil.Reverse(h, relutil.SortByRevision) - return h[0], nil + relutil.Reverse(rls, relutil.SortByRevision) + return rls[0], nil } // makeKey concatenates the Kubernetes storage object type, a release name and version @@ -259,27 +335,16 @@ func Init(d driver.Driver) *Storage { if d == nil { d = driver.NewMemory() } - return &Storage{ + s := &Storage{ Driver: d, - Log: func(_ string, _ ...interface{}) {}, - } -} - -func (s *Storage) HistoryUntilRevision(name string, ignoreSinceRevision int) ([]*rspb.Release, error) { - history, err := s.History(name) - if err != nil { - return nil, fmt.Errorf("error getting release history: %w", err) } - relutil.SortByRevision(history) - - resultLength := len(history) - for i, release := range history { - if release.Version == ignoreSinceRevision { - resultLength = i - break - } + // Get logger from driver if it implements the LoggerSetterGetter interface + if ls, ok := d.(logging.LoggerSetterGetter); ok { + ls.SetLogger(s.Logger().Handler()) + } else { + // If the driver does not implement the LoggerSetterGetter interface, set the default logger + s.SetLogger(slog.Default().Handler()) } - - return history[:resultLength], nil + return s } diff --git a/pkg/helm/pkg/storage/storage_test.go b/pkg/helm/pkg/storage/storage_test.go index 86bc3bf7..f417b68b 100644 --- a/pkg/helm/pkg/storage/storage_test.go +++ b/pkg/helm/pkg/storage/storage_test.go @@ -14,16 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -package storage // import "helm.sh/helm/v3/pkg/storage" +package storage // import "github.com/werf/nelm/pkg/helm/pkg/storage" import ( + "errors" "fmt" "reflect" "testing" - "github.com/pkg/errors" + "github.com/stretchr/testify/assert" - rspb "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/helm/pkg/release/common" + rspb "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/helm/pkg/storage/driver" ) @@ -57,13 +60,13 @@ func TestStorageUpdate(t *testing.T) { rls := ReleaseTestData{ Name: "angry-beaver", Version: 1, - Status: rspb.StatusDeployed, + Status: common.StatusDeployed, }.ToRelease() assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") // modify the release - rls.Info.Status = rspb.StatusUninstalled + rls.Info.Status = common.StatusUninstalled assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") // retrieve the updated release @@ -107,13 +110,16 @@ func TestStorageDelete(t *testing.T) { t.Errorf("unexpected error: %s", err) } + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + // We have now deleted one of the two records. - if len(hist) != 1 { + if len(rhist) != 1 { t.Errorf("expected 1 record for deleted release version, got %d", len(hist)) } - if hist[0].Version != 2 { - t.Errorf("Expected version to be 2, got %d", hist[0].Version) + if rhist[0].Version != 2 { + t.Errorf("Expected version to be 2, got %d", rhist[0].Version) } } @@ -124,13 +130,13 @@ func TestStorageList(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.StatusDeployed}.ToRelease() - rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.StatusDeployed}.ToRelease() - rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.StatusUninstalled}.ToRelease() - rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.StatusUninstalled}.ToRelease() + rls0 := ReleaseTestData{Name: "happy-catdog", Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: "livid-human", Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: "relaxed-cat", Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: "hungry-hippo", Status: common.StatusDeployed}.ToRelease() + rls4 := ReleaseTestData{Name: "angry-beaver", Status: common.StatusDeployed}.ToRelease() + rls5 := ReleaseTestData{Name: "opulent-frog", Status: common.StatusUninstalled}.ToRelease() + rls6 := ReleaseTestData{Name: "happy-liger", Status: common.StatusUninstalled}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") @@ -145,7 +151,7 @@ func TestStorageList(t *testing.T) { var listTests = []struct { Description string NumExpected int - ListFunc func() ([]*rspb.Release, error) + ListFunc func() ([]release.Releaser, error) }{ {"ListDeployed", 2, storage.ListDeployed}, {"ListReleases", 7, storage.ListReleases}, @@ -176,10 +182,10 @@ func TestStorageDeployed(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -195,15 +201,18 @@ func TestStorageDeployed(t *testing.T) { t.Fatalf("Failed to query for deployed release: %s\n", err) } + rel, err := releaserToV1Release(rls) + assert.NoError(t, err) + switch { case rls == nil: t.Fatalf("Release is nil") - case rls.Name != name: - t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) - case rls.Version != vers: - t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) - case rls.Info.Status != rspb.StatusDeployed: - t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) + case rel.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rel.Name) + case rel.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rel.Version) + case rel.Info.Status != common.StatusDeployed: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rel.Info.Status.String()) } } @@ -216,10 +225,10 @@ func TestStorageDeployedWithCorruption(t *testing.T) { // setup storage with test releases setup := func() { // release records (notice odd order and corruption) - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -235,15 +244,18 @@ func TestStorageDeployedWithCorruption(t *testing.T) { t.Fatalf("Failed to query for deployed release: %s\n", err) } + rel, err := releaserToV1Release(rls) + assert.NoError(t, err) + switch { case rls == nil: t.Fatalf("Release is nil") - case rls.Name != name: - t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) - case rls.Version != vers: - t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) - case rls.Info.Status != rspb.StatusDeployed: - t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) + case rel.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rel.Name) + case rel.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rel.Version) + case rel.Info.Status != common.StatusDeployed: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rel.Info.Status.String()) } } @@ -255,10 +267,10 @@ func TestStorageHistory(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -287,22 +299,22 @@ type MaxHistoryMockDriver struct { func NewMaxHistoryMockDriver(d driver.Driver) *MaxHistoryMockDriver { return &MaxHistoryMockDriver{Driver: d} } -func (d *MaxHistoryMockDriver) Create(key string, rls *rspb.Release) error { +func (d *MaxHistoryMockDriver) Create(key string, rls release.Releaser) error { return d.Driver.Create(key, rls) } -func (d *MaxHistoryMockDriver) Update(key string, rls *rspb.Release) error { +func (d *MaxHistoryMockDriver) Update(key string, rls release.Releaser) error { return d.Driver.Update(key, rls) } -func (d *MaxHistoryMockDriver) Delete(_ string) (*rspb.Release, error) { +func (d *MaxHistoryMockDriver) Delete(_ string) (release.Releaser, error) { return nil, errMaxHistoryMockDriverSomethingHappened } -func (d *MaxHistoryMockDriver) Get(key string) (*rspb.Release, error) { +func (d *MaxHistoryMockDriver) Get(key string) (release.Releaser, error) { return d.Driver.Get(key) } -func (d *MaxHistoryMockDriver) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (d *MaxHistoryMockDriver) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { return d.Driver.List(filter) } -func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]*rspb.Release, error) { +func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]release.Releaser, error) { return d.Driver.Query(labels) } func (d *MaxHistoryMockDriver) Name() string { @@ -310,9 +322,8 @@ func (d *MaxHistoryMockDriver) Name() string { } func TestMaxHistoryErrorHandling(t *testing.T) { - // func TestStorageRemoveLeastRecentWithError(t *testing.T) { + //func TestStorageRemoveLeastRecentWithError(t *testing.T) { storage := Init(NewMaxHistoryMockDriver(driver.NewMemory())) - storage.Log = t.Logf storage.MaxHistory = 1 @@ -321,14 +332,14 @@ func TestMaxHistoryErrorHandling(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls1 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Driver.Create(makeKey(rls1.Name, rls1.Version), rls1), "Storing release 'angry-bird' (v1)") } setup() - rls2 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() wantErr := errMaxHistoryMockDriverSomethingHappened gotErr := storage.Create(rls2) if !errors.Is(gotErr, wantErr) { @@ -338,7 +349,6 @@ func TestMaxHistoryErrorHandling(t *testing.T) { func TestStorageRemoveLeastRecent(t *testing.T) { storage := Init(driver.NewMemory()) - storage.Log = t.Logf // Make sure that specifying this at the outset doesn't cause any bugs. storage.MaxHistory = 10 @@ -348,10 +358,10 @@ func TestStorageRemoveLeastRecent(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -370,22 +380,25 @@ func TestStorageRemoveLeastRecent(t *testing.T) { } storage.MaxHistory = 3 - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusDeployed}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusDeployed}.ToRelease() assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") // On inserting the 5th record, we expect two records to be pruned from history. hist, err := storage.History(name) + assert.NoError(t, err) + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) if err != nil { t.Fatal(err) - } else if len(hist) != storage.MaxHistory { - for _, item := range hist { + } else if len(rhist) != storage.MaxHistory { + for _, item := range rhist { t.Logf("%s %v", item.Name, item.Version) } - t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) + t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(rhist)) } // We expect the existing records to be 3, 4, and 5. - for i, item := range hist { + for i, item := range rhist { v := item.Version if expect := i + 3; v != expect { t.Errorf("Expected release %d, got %d", expect, v) @@ -395,7 +408,6 @@ func TestStorageRemoveLeastRecent(t *testing.T) { func TestStorageDoNotDeleteDeployed(t *testing.T) { storage := Init(driver.NewMemory()) - storage.Log = t.Logf storage.MaxHistory = 3 const name = "angry-bird" @@ -403,10 +415,10 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusFailed}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -416,7 +428,7 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { } setup() - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusFailed}.ToRelease() assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") // On inserting the 5th record, we expect a total of 3 releases, but we expect version 2 @@ -425,10 +437,12 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { if err != nil { t.Fatal(err) } else if len(hist) != storage.MaxHistory { - for _, item := range hist { + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + for _, item := range rhist { t.Logf("%s %v", item.Name, item.Version) } - t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) + t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(rhist)) } expectedVersions := map[int]bool{ @@ -437,7 +451,9 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { 5: true, } - for _, item := range hist { + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + for _, item := range rhist { if !expectedVersions[item.Version] { t.Errorf("Release version %d, found when not expected", item.Version) } @@ -452,10 +468,10 @@ func TestStorageLast(t *testing.T) { // Set up storage with test releases. setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -471,12 +487,15 @@ func TestStorageLast(t *testing.T) { t.Fatalf("Failed to query for release history (%q): %s\n", name, err) } - if h.Version != 4 { - t.Errorf("Expected revision 4, got %d", h.Version) + rel, err := releaserToV1Release(h) + assert.NoError(t, err) + + if rel.Version != 4 { + t.Errorf("Expected revision 4, got %d", rel.Version) } } -// TestUpgradeInitiallyFailedRelease tests a case when there are no deployed release yet, but history limit has been +// TestUpgradeInitiallyFailedReleaseWithHistoryLimit tests a case when there are no deployed release yet, but history limit has been // reached: the has-no-deployed-releases error should not occur in such case. func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { storage := Init(driver.NewMemory()) @@ -487,10 +506,10 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusFailed}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusFailed}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusFailed}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusFailed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusFailed}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -511,7 +530,7 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { setup() - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusFailed}.ToRelease() err := storage.Create(rls5) if err != nil { t.Fatalf("Failed to create a new release version: %s", err) @@ -522,13 +541,15 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - for i, rel := range hist { + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + for i, rel := range rhist { wantVersion := i + 2 if rel.Version != wantVersion { t.Fatalf("Expected history release %d version to equal %d, got %d", i+1, wantVersion, rel.Version) } - wantStatus := rspb.StatusFailed + wantStatus := common.StatusFailed if rel.Info.Status != wantStatus { t.Fatalf("Expected history release %d status to equal %q, got %q", i+1, wantStatus, rel.Info.Status) } @@ -540,7 +561,7 @@ type ReleaseTestData struct { Version int Manifest string Namespace string - Status rspb.Status + Status common.Status } func (test ReleaseTestData) ToRelease() *rspb.Release { diff --git a/pkg/helm/pkg/strvals/fuzz_test.go b/pkg/helm/pkg/strvals/fuzz_test.go new file mode 100644 index 00000000..68b43c8e --- /dev/null +++ b/pkg/helm/pkg/strvals/fuzz_test.go @@ -0,0 +1,26 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package strvals + +import ( + "testing" +) + +func FuzzParse(f *testing.F) { + f.Fuzz(func(_ *testing.T, data string) { + _, _ = Parse(data) + }) +} diff --git a/pkg/helm/pkg/strvals/literal_parser.go b/pkg/helm/pkg/strvals/literal_parser.go index f7565581..d5d4c25b 100644 --- a/pkg/helm/pkg/strvals/literal_parser.go +++ b/pkg/helm/pkg/strvals/literal_parser.go @@ -17,11 +17,10 @@ package strvals import ( "bytes" + "errors" "fmt" "io" "strconv" - - "github.com/pkg/errors" ) // ParseLiteral parses a set line interpreting the value as a literal string. @@ -68,7 +67,7 @@ func (t *literalParser) parse() error { if err == nil { continue } - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil } return err @@ -102,7 +101,7 @@ func (t *literalParser) key(data map[string]interface{}, nestedNameLevel int) (r if len(key) == 0 { return err } - return errors.Errorf("key %q has no value", string(key)) + return fmt.Errorf("key %q has no value", string(key)) case lastRune == '=': // found end of key: swallow the '=' and get the value @@ -129,7 +128,7 @@ func (t *literalParser) key(data map[string]interface{}, nestedNameLevel int) (r // recurse on sub-tree with remaining data err := t.key(inner, nestedNameLevel) if err == nil && len(inner) == 0 { - return errors.Errorf("key map %q has no value", string(key)) + return fmt.Errorf("key map %q has no value", string(key)) } if len(inner) != 0 { set(data, string(key), inner) @@ -140,7 +139,7 @@ func (t *literalParser) key(data map[string]interface{}, nestedNameLevel int) (r // We are in a list index context, so we need to set an index. i, err := t.keyIndex() if err != nil { - return errors.Wrap(err, "error parsing index") + return fmt.Errorf("error parsing index: %w", err) } kk := string(key) @@ -178,14 +177,14 @@ func (t *literalParser) listItem(list []interface{}, i, nestedNameLevel int) ([] switch key, lastRune, err := runesUntilLiteral(t.sc, stop); { case len(key) > 0: - return list, errors.Errorf("unexpected data at end of array index: %q", key) + return list, fmt.Errorf("unexpected data at end of array index: %q", key) case err != nil: return list, err case lastRune == '=': value, err := t.val() - if err != nil && err != io.EOF { + if err != nil && !errors.Is(err, io.EOF) { return list, err } return setIndex(list, i, string(value)) @@ -214,7 +213,7 @@ func (t *literalParser) listItem(list []interface{}, i, nestedNameLevel int) ([] // now we have a nested list. Read the index and handle. nextI, err := t.keyIndex() if err != nil { - return list, errors.Wrap(err, "error parsing index") + return list, fmt.Errorf("error parsing index: %w", err) } var crtList []interface{} if len(list) > i { @@ -233,7 +232,7 @@ func (t *literalParser) listItem(list []interface{}, i, nestedNameLevel int) ([] return setIndex(list, i, list2) default: - return nil, errors.Errorf("parse error: unexpected token %v", lastRune) + return nil, fmt.Errorf("parse error: unexpected token %v", lastRune) } } diff --git a/pkg/helm/pkg/strvals/literal_parser_test.go b/pkg/helm/pkg/strvals/literal_parser_test.go index 4e74423d..6a76458f 100644 --- a/pkg/helm/pkg/strvals/literal_parser_test.go +++ b/pkg/helm/pkg/strvals/literal_parser_test.go @@ -17,6 +17,7 @@ package strvals import ( "fmt" + "strings" "testing" "sigs.k8s.io/yaml" @@ -416,14 +417,14 @@ func TestParseLiteralInto(t *testing.T) { } func TestParseLiteralNestedLevels(t *testing.T) { - var keyMultipleNestedLevels string + var keyMultipleNestedLevels strings.Builder for i := 1; i <= MaxNestedNameLevel+2; i++ { tmpStr := fmt.Sprintf("name%d", i) if i <= MaxNestedNameLevel+1 { tmpStr = tmpStr + "." } - keyMultipleNestedLevels += tmpStr + keyMultipleNestedLevels.WriteString(tmpStr) } tests := []struct { @@ -439,7 +440,7 @@ func TestParseLiteralNestedLevels(t *testing.T) { "", }, { - str: keyMultipleNestedLevels + "=value", + str: keyMultipleNestedLevels.String() + "=value", err: true, errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), }, diff --git a/pkg/helm/pkg/strvals/parser.go b/pkg/helm/pkg/strvals/parser.go index 2828f20c..8eb761dc 100644 --- a/pkg/helm/pkg/strvals/parser.go +++ b/pkg/helm/pkg/strvals/parser.go @@ -18,13 +18,13 @@ package strvals import ( "bytes" "encoding/json" + "errors" "fmt" "io" "strconv" "strings" "unicode" - "github.com/pkg/errors" "sigs.k8s.io/yaml" ) @@ -161,7 +161,7 @@ func (t *parser) parse() error { if err == nil { continue } - if err == io.EOF { + if errors.Is(err, io.EOF) { return nil } return err @@ -189,14 +189,14 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e if len(k) == 0 { return err } - return errors.Errorf("key %q has no value", string(k)) + return fmt.Errorf("key %q has no value", string(k)) //set(data, string(k), "") //return err case last == '[': // We are in a list index context, so we need to set an index. i, err := t.keyIndex() if err != nil { - return errors.Wrap(err, "error parsing index") + return fmt.Errorf("error parsing index: %w", err) } kk := string(k) // Find or create target list @@ -237,7 +237,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e _, err = t.emptyVal() return err } - //End of key. Consume =, Get value. + // End of key. Consume =, Get value. // FIXME: Get value list first vl, e := t.valList() switch e { @@ -261,7 +261,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e case last == ',': // No value given. Set the value to empty string. Return error. set(data, string(k), "") - return errors.Errorf("key %q has no value (cannot end with ,)", string(k)) + return fmt.Errorf("key %q has no value (cannot end with ,)", string(k)) case last == '.': // Check value name is within the maximum nested name level nestedNameLevel++ @@ -278,7 +278,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e // Recurse e := t.key(inner, nestedNameLevel) if e == nil && len(inner) == 0 { - return errors.Errorf("key map %q has no value", string(k)) + return fmt.Errorf("key map %q has no value", string(k)) } if len(inner) != 0 { set(data, string(k), inner) @@ -332,6 +332,7 @@ func (t *parser) keyIndex() (int, error) { return strconv.Atoi(string(v)) } + func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interface{}, error) { if i < 0 { return list, fmt.Errorf("negative %d index not allowed", i) @@ -339,7 +340,7 @@ func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interfa stop := runeSet([]rune{'[', '.', '='}) switch k, last, err := runesUntil(t.sc, stop); { case len(k) > 0: - return list, errors.Errorf("unexpected data at end of array index: %q", k) + return list, fmt.Errorf("unexpected data at end of array index: %q", k) case err != nil: return list, err case last == '=': @@ -394,7 +395,7 @@ func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interfa // now we have a nested list. Read the index and handle. nextI, err := t.keyIndex() if err != nil { - return list, errors.Wrap(err, "error parsing index") + return list, fmt.Errorf("error parsing index: %w", err) } var crtList []interface{} if len(list) > i { @@ -430,13 +431,13 @@ func (t *parser) listItem(list []interface{}, i, nestedNameLevel int) ([]interfa } return setIndex(list, i, inner) default: - return nil, errors.Errorf("parse error: unexpected token %v", last) + return nil, fmt.Errorf("parse error: unexpected token %v", last) } } // check for an empty value // read and consume optional spaces until comma or EOF (empty val) or any other char (not empty val) -// comma and spaces are consumed, while any other char is not cosumed +// comma and spaces are consumed, while any other char is not consumed func (t *parser) emptyVal() (bool, error) { for { r, _, e := t.sc.ReadRune() diff --git a/pkg/helm/pkg/strvals/parser_test.go b/pkg/helm/pkg/strvals/parser_test.go index 925aa97c..73403fc5 100644 --- a/pkg/helm/pkg/strvals/parser_test.go +++ b/pkg/helm/pkg/strvals/parser_test.go @@ -17,6 +17,7 @@ package strvals import ( "fmt" + "strings" "testing" "sigs.k8s.io/yaml" @@ -626,7 +627,7 @@ func TestParseJSON(t *testing.T) { }, err: false, }, - { // null assigment, and no value assigned (equivalent to null) + { // null assignment, and no value assigned (equivalent to null) input: "outer.inner1=,outer.inner3={\"aa\":\"1\",\"bb\":2,\"cc\":[1,2,3]},outer.inner3.cc[1]=null", got: map[string]interface{}{ "outer": map[string]interface{}{ @@ -757,13 +758,13 @@ func TestToYAML(t *testing.T) { } func TestParseSetNestedLevels(t *testing.T) { - var keyMultipleNestedLevels string + var keyMultipleNestedLevels strings.Builder for i := 1; i <= MaxNestedNameLevel+2; i++ { tmpStr := fmt.Sprintf("name%d", i) if i <= MaxNestedNameLevel+1 { tmpStr = tmpStr + "." } - keyMultipleNestedLevels += tmpStr + keyMultipleNestedLevels.WriteString(tmpStr) } tests := []struct { str string @@ -778,7 +779,7 @@ func TestParseSetNestedLevels(t *testing.T) { "", }, { - str: keyMultipleNestedLevels + "=value", + str: keyMultipleNestedLevels.String() + "=value", err: true, errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), diff --git a/pkg/helm/pkg/time/time.go b/pkg/helm/pkg/time/time.go deleted file mode 100644 index 44f3fedf..00000000 --- a/pkg/helm/pkg/time/time.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package time contains a wrapper for time.Time in the standard library and -// associated methods. This package mainly exists to workaround an issue in Go -// where the serializer doesn't omit an empty value for time: -// https://github.com/golang/go/issues/11939. As such, this can be removed if a -// proposal is ever accepted for Go -package time - -import ( - "bytes" - "time" -) - -// emptyString contains an empty JSON string value to be used as output -var emptyString = `""` - -// Time is a convenience wrapper around stdlib time, but with different -// marshalling and unmarshaling for zero values -type Time struct { - time.Time -} - -// Now returns the current time. It is a convenience wrapper around time.Now() -func Now() Time { - return Time{time.Now()} -} - -func (t Time) MarshalJSON() ([]byte, error) { - if t.Time.IsZero() { - return []byte(emptyString), nil - } - - return t.Time.MarshalJSON() -} - -func (t *Time) UnmarshalJSON(b []byte) error { - if bytes.Equal(b, []byte("null")) { - return nil - } - // If it is empty, we don't have to set anything since time.Time is not a - // pointer and will be set to the zero value - if bytes.Equal([]byte(emptyString), b) { - return nil - } - - return t.Time.UnmarshalJSON(b) -} - -func Parse(layout, value string) (Time, error) { - t, err := time.Parse(layout, value) - return Time{Time: t}, err -} -func ParseInLocation(layout, value string, loc *time.Location) (Time, error) { - t, err := time.ParseInLocation(layout, value, loc) - return Time{Time: t}, err -} - -func Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) Time { - return Time{Time: time.Date(year, month, day, hour, min, sec, nsec, loc)} -} - -func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} } - -func (t Time) Add(d time.Duration) Time { return Time{Time: t.Time.Add(d)} } -func (t Time) AddDate(years int, months int, days int) Time { - return Time{Time: t.Time.AddDate(years, months, days)} -} -func (t Time) After(u Time) bool { return t.Time.After(u.Time) } -func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) } -func (t Time) Equal(u Time) bool { return t.Time.Equal(u.Time) } -func (t Time) In(loc *time.Location) Time { return Time{Time: t.Time.In(loc)} } -func (t Time) Local() Time { return Time{Time: t.Time.Local()} } -func (t Time) Round(d time.Duration) Time { return Time{Time: t.Time.Round(d)} } -func (t Time) Sub(u Time) time.Duration { return t.Time.Sub(u.Time) } -func (t Time) Truncate(d time.Duration) Time { return Time{Time: t.Time.Truncate(d)} } -func (t Time) UTC() Time { return Time{Time: t.Time.UTC()} } diff --git a/pkg/helm/pkg/time/time_test.go b/pkg/helm/pkg/time/time_test.go deleted file mode 100644 index 20f0f8e2..00000000 --- a/pkg/helm/pkg/time/time_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package time - -import ( - "encoding/json" - "testing" - "time" -) - -var ( - testingTime, _ = Parse(time.RFC3339, "1977-09-02T22:04:05Z") - testingTimeString = `"1977-09-02T22:04:05Z"` -) - -func TestNonZeroValueMarshal(t *testing.T) { - res, err := json.Marshal(testingTime) - if err != nil { - t.Fatal(err) - } - if testingTimeString != string(res) { - t.Errorf("expected a marshaled value of %s, got %s", testingTimeString, res) - } -} - -func TestZeroValueMarshal(t *testing.T) { - res, err := json.Marshal(Time{}) - if err != nil { - t.Fatal(err) - } - if string(res) != emptyString { - t.Errorf("expected zero value to marshal to empty string, got %s", res) - } -} - -func TestNonZeroValueUnmarshal(t *testing.T) { - var myTime Time - err := json.Unmarshal([]byte(testingTimeString), &myTime) - if err != nil { - t.Fatal(err) - } - if !myTime.Equal(testingTime) { - t.Errorf("expected time to be equal to %v, got %v", testingTime, myTime) - } -} - -func TestEmptyStringUnmarshal(t *testing.T) { - var myTime Time - err := json.Unmarshal([]byte(emptyString), &myTime) - if err != nil { - t.Fatal(err) - } - if !myTime.IsZero() { - t.Errorf("expected time to be equal to zero value, got %v", myTime) - } -} - -func TestZeroValueUnmarshal(t *testing.T) { - // This test ensures that we can unmarshal any time value that was output - // with the current go default value of "0001-01-01T00:00:00Z" - var myTime Time - err := json.Unmarshal([]byte(`"0001-01-01T00:00:00Z"`), &myTime) - if err != nil { - t.Fatal(err) - } - if !myTime.IsZero() { - t.Errorf("expected time to be equal to zero value, got %v", myTime) - } -} diff --git a/pkg/helm/pkg/uploader/chart_uploader.go b/pkg/helm/pkg/uploader/chart_uploader.go index 81191c87..927d5981 100644 --- a/pkg/helm/pkg/uploader/chart_uploader.go +++ b/pkg/helm/pkg/uploader/chart_uploader.go @@ -20,11 +20,8 @@ import ( "io" "net/url" - "github.com/pkg/errors" - "github.com/werf/nelm/pkg/helm/pkg/pusher" "github.com/werf/nelm/pkg/helm/pkg/registry" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" ) // ChartUploader handles uploading a chart. @@ -40,10 +37,10 @@ type ChartUploader struct { } // UploadTo uploads a chart. Depending on the settings, it may also upload a provenance file. -func (c *ChartUploader) UploadTo(ref, remote string, opts helmopts.HelmOptions) error { +func (c *ChartUploader) UploadTo(ref, remote string) error { u, err := url.Parse(remote) if err != nil { - return errors.Errorf("invalid chart URL format: %s", remote) + return fmt.Errorf("invalid chart URL format: %s", remote) } if u.Scheme == "" { @@ -55,5 +52,5 @@ func (c *ChartUploader) UploadTo(ref, remote string, opts helmopts.HelmOptions) return err } - return p.Push(ref, u.String(), opts, c.Options...) + return p.Push(ref, u.String(), c.Options...) } diff --git a/pkg/helm/pkg/werf/chartextender/chart_metadata.go b/pkg/helm/pkg/werf/chartextender/chart_metadata.go deleted file mode 100644 index ddf2666e..00000000 --- a/pkg/helm/pkg/werf/chartextender/chart_metadata.go +++ /dev/null @@ -1,37 +0,0 @@ -package chartextender - -import "github.com/werf/nelm/pkg/helm/pkg/chart" - -type GetHelmChartMetadataOptions struct { - OverrideAppVersion string - DefaultAPIVersion string - DefaultName string - DefaultVersion string -} - -func AutosetChartMetadata(metadataIn *chart.Metadata, opts GetHelmChartMetadataOptions) *chart.Metadata { - var metadata *chart.Metadata - if metadataIn == nil { - metadata = &chart.Metadata{} - } else { - metadata = metadataIn - } - - if metadata.APIVersion == "" { - metadata.APIVersion = opts.DefaultAPIVersion - } - - if metadata.Name == "" { - metadata.Name = opts.DefaultName - } - - if opts.OverrideAppVersion != "" { - metadata.AppVersion = opts.OverrideAppVersion - } - - if metadata.Version == "" { - metadata.Version = opts.DefaultVersion - } - - return metadata -} diff --git a/pkg/helm/pkg/werf/file/buffered_file.go b/pkg/helm/pkg/werf/file/buffered_file.go deleted file mode 100644 index 94bfdd7c..00000000 --- a/pkg/helm/pkg/werf/file/buffered_file.go +++ /dev/null @@ -1,6 +0,0 @@ -package file - -type ChartExtenderBufferedFile struct { - Name string - Data []byte -} diff --git a/pkg/helm/pkg/werf/file/chart_file_reader.go b/pkg/helm/pkg/werf/file/chart_file_reader.go deleted file mode 100644 index 47a9b6ac..00000000 --- a/pkg/helm/pkg/werf/file/chart_file_reader.go +++ /dev/null @@ -1,15 +0,0 @@ -package file - -import ( - "context" -) - -type ChartFileReaderInterface interface { - LocateChart(ctx context.Context, name string) (string, error) - ReadChartFile(ctx context.Context, filePath string) ([]byte, error) - LoadChartDir(ctx context.Context, dir string) ([]*ChartExtenderBufferedFile, error) - ChartIsDir(relPath string) (bool, error) -} - -// TODO(werf): keep it global, but separate package? Make non-giterminism default implementation -var ChartFileReader ChartFileReaderInterface diff --git a/pkg/helm/pkg/werf/file/chart_file_writer.go b/pkg/helm/pkg/werf/file/chart_file_writer.go deleted file mode 100644 index f55ef157..00000000 --- a/pkg/helm/pkg/werf/file/chart_file_writer.go +++ /dev/null @@ -1,12 +0,0 @@ -package file - -import ( - "context" -) - -type ChartFileWriterInterface interface { - WriteChartFile(ctx context.Context, filePath string, data []byte) error - CreateChartDir(ctx context.Context, dir string) error -} - -var ChartFileWriter ChartFileWriterInterface diff --git a/pkg/helm/pkg/werf/helmopts/helmoptions.go b/pkg/helm/pkg/werf/helmopts/helmoptions.go deleted file mode 100644 index c9486266..00000000 --- a/pkg/helm/pkg/werf/helmopts/helmoptions.go +++ /dev/null @@ -1,43 +0,0 @@ -package helmopts - -type HelmOptions struct { - ChartLoadOpts ChartLoadOptions - TypeScriptOpts TypeScriptOptions -} - -type ChartLoadOptions struct { - ChartAppVersion string - ChartType ChartType - DefaultChartAPIVersion string - DefaultChartName string - DefaultChartVersion string - DefaultSecretValuesDisable bool - DefaultValuesDisable bool - DepDownloader DepDownloader - ExtraValues map[string]interface{} - NoSecrets bool - SecretKeyIgnore bool - SecretValuesFiles []string - SecretWorkDir string - DefaultRootContext map[string]interface{} -} - -type TypeScriptOptions struct { - DenoBinaryPath string -} - -type ChartType string - -const ( - ChartTypeChart ChartType = "" - ChartTypeBundle ChartType = "bundle" - ChartTypeSubchart ChartType = "subchart" - ChartTypeChartStub ChartType = "chartstub" -) - -type DepDownloader interface { - Build(opts HelmOptions) error - Update(opts HelmOptions) error - UpdateRepositories() error - SetChartPath(path string) -} diff --git a/pkg/helm/pkg/werf/secrets/chart_secrets_loader.go b/pkg/helm/pkg/werf/secrets/chart_secrets_loader.go deleted file mode 100644 index 4a2fe277..00000000 --- a/pkg/helm/pkg/werf/secrets/chart_secrets_loader.go +++ /dev/null @@ -1,63 +0,0 @@ -package secrets - -import ( - "fmt" - "path/filepath" - "strings" - "unicode" - - "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/common-go/pkg/secret" - "github.com/werf/common-go/pkg/util" -) - -const ( - DefaultSecretValuesFileName = "secret-values.yaml" - SecretDirName = "secret" -) - -func GetDefaultSecretValuesFile(loadedChartFiles []*file.ChartExtenderBufferedFile) *file.ChartExtenderBufferedFile { - for _, file := range loadedChartFiles { - if file.Name == DefaultSecretValuesFileName { - return file - } - } - - return nil -} - -func GetSecretDirFiles(loadedChartFiles []*file.ChartExtenderBufferedFile) []*file.ChartExtenderBufferedFile { - var res []*file.ChartExtenderBufferedFile - - for _, file := range loadedChartFiles { - if !util.IsSubpathOfBasePath(SecretDirName, file.Name) { - continue - } - res = append(res, file) - } - - return res -} - -func LoadChartSecretDirFilesData( - secretFiles []*file.ChartExtenderBufferedFile, - encoder *secret.YamlEncoder, -) (map[string]string, error) { - res := make(map[string]string) - - for _, file := range secretFiles { - if !util.IsSubpathOfBasePath(SecretDirName, file.Name) { - continue - } - - decodedData, err := encoder.Decrypt([]byte(strings.TrimRightFunc(string(file.Data), unicode.IsSpace))) - if err != nil { - return nil, fmt.Errorf("error decoding %s: %w", file.Name, err) - } - - relPath := util.GetRelativeToBaseFilepath(SecretDirName, file.Name) - res[filepath.ToSlash(relPath)] = string(decodedData) - } - - return res, nil -} diff --git a/pkg/helm/pkg/werf/secrets/gotmplfunctions/go_tmpl_functions.go b/pkg/helm/pkg/werf/secrets/gotmplfunctions/go_tmpl_functions.go deleted file mode 100644 index 3e0b278a..00000000 --- a/pkg/helm/pkg/werf/secrets/gotmplfunctions/go_tmpl_functions.go +++ /dev/null @@ -1,31 +0,0 @@ -package gotmplfunctions - -import ( - "fmt" - "path" - "strings" - "text/template" - - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/runtimedata" -) - -func SetupWerfSecretFile(secretsRuntimeData runtimedata.RuntimeData, funcMap template.FuncMap) { - funcMap["werf_secret_file"] = func(secretRelativePath string) (string, error) { - if path.IsAbs(secretRelativePath) { - return "", fmt.Errorf("expected relative secret file path, given path %v", secretRelativePath) - } - - decodedData, ok := secretsRuntimeData.GetDecryptedSecretFilesData()[secretRelativePath] - - if !ok { - var secretFiles []string - for key := range secretsRuntimeData.GetDecryptedSecretFilesData() { - secretFiles = append(secretFiles, key) - } - - return "", fmt.Errorf("secret file %q not found, you may use one of the following: %q", secretRelativePath, strings.Join(secretFiles, "', '")) - } - - return decodedData, nil - } -} diff --git a/pkg/helm/pkg/werf/secrets/runtimedata/interface.go b/pkg/helm/pkg/werf/secrets/runtimedata/interface.go deleted file mode 100644 index e5f2daf8..00000000 --- a/pkg/helm/pkg/werf/secrets/runtimedata/interface.go +++ /dev/null @@ -1,24 +0,0 @@ -package runtimedata - -import ( - "context" - - "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/common-go/pkg/secrets_manager" -) - -type RuntimeData interface { - DecodeAndLoadSecrets(ctx context.Context, loadedChartFiles []*file.ChartExtenderBufferedFile, secretsManager *secrets_manager.SecretsManager, opts DecodeAndLoadSecretsOptions) error - GetEncodedSecretValues(ctx context.Context, secretsManager *secrets_manager.SecretsManager, secretsWorkingDir string, noDecryptSecrets bool) (map[string]interface{}, error) - GetDecryptedSecretValues() map[string]interface{} - GetDecryptedSecretFilesData() map[string]string - GetSecretValuesToMask() []string -} - -type DecodeAndLoadSecretsOptions struct { - CustomSecretValueFiles []string - LoadFromLocalFilesystem bool - NoDecryptSecrets bool - SecretsWorkingDir string - WithoutDefaultSecretValues bool -} diff --git a/pkg/helm/pkg/werf/secrets/secrets_runtime_data.go b/pkg/helm/pkg/werf/secrets/secrets_runtime_data.go deleted file mode 100644 index 28d994b6..00000000 --- a/pkg/helm/pkg/werf/secrets/secrets_runtime_data.go +++ /dev/null @@ -1,171 +0,0 @@ -package secrets - -import ( - "context" - "fmt" - "io/ioutil" - - "sigs.k8s.io/yaml" - - werffile "github.com/werf/nelm/pkg/helm/pkg/werf/file" - "github.com/werf/nelm/pkg/helm/pkg/werf/secrets/runtimedata" - "github.com/werf/common-go/pkg/secret" - "github.com/werf/common-go/pkg/secrets_manager" - "github.com/werf/common-go/pkg/secretvalues" -) - -var _ runtimedata.RuntimeData = (*SecretsRuntimeData)(nil) - -var CoalesceTablesFunc func(dst, src map[string]interface{}) map[string]interface{} - -type SecretsRuntimeData struct { - decryptedSecretValues map[string]interface{} - decryptedSecretFilesData map[string]string - secretValuesToMask []string -} - -func NewSecretsRuntimeData() *SecretsRuntimeData { - return &SecretsRuntimeData{ - decryptedSecretFilesData: make(map[string]string), - } -} - -func (secretsRuntimeData *SecretsRuntimeData) DecodeAndLoadSecrets( - ctx context.Context, - loadedChartFiles []*werffile.ChartExtenderBufferedFile, - secretsManager *secrets_manager.SecretsManager, - opts runtimedata.DecodeAndLoadSecretsOptions, -) error { - secretDirFiles := GetSecretDirFiles(loadedChartFiles) - - var loadedSecretValuesFiles []*werffile.ChartExtenderBufferedFile - - if !opts.WithoutDefaultSecretValues { - if defaultSecretValues := GetDefaultSecretValuesFile(loadedChartFiles); defaultSecretValues != nil { - loadedSecretValuesFiles = append(loadedSecretValuesFiles, defaultSecretValues) - } - } - - for _, customSecretValuesFileName := range opts.CustomSecretValueFiles { - file := &werffile.ChartExtenderBufferedFile{Name: customSecretValuesFileName} - - if opts.LoadFromLocalFilesystem { - data, err := ioutil.ReadFile(customSecretValuesFileName) - if err != nil { - return fmt.Errorf("unable to read custom secret values file %q from local filesystem: %w", customSecretValuesFileName, err) - } - file.Data = data - } else { - data, err := werffile.ChartFileReader.ReadChartFile(ctx, customSecretValuesFileName) - if err != nil { - return fmt.Errorf("unable to read custom secret values file %q: %w", customSecretValuesFileName, err) - } - file.Data = data - } - - loadedSecretValuesFiles = append(loadedSecretValuesFiles, file) - } - - var encoder *secret.YamlEncoder - if len(secretDirFiles)+len(loadedSecretValuesFiles) > 0 { - if enc, err := secretsManager.GetYamlEncoder(ctx, opts.SecretsWorkingDir, opts.NoDecryptSecrets); err != nil { - return fmt.Errorf("error getting secrets yaml encoder: %w", err) - } else { - encoder = enc - } - } - - if len(secretDirFiles) > 0 { - if data, err := LoadChartSecretDirFilesData(secretDirFiles, encoder); err != nil { - return fmt.Errorf("error loading secret files data: %w", err) - } else { - secretsRuntimeData.decryptedSecretFilesData = data - for _, fileData := range secretsRuntimeData.decryptedSecretFilesData { - secretsRuntimeData.secretValuesToMask = append(secretsRuntimeData.secretValuesToMask, fileData) - } - } - } - - if len(loadedSecretValuesFiles) > 0 { - if values, err := LoadChartSecretValueFiles(loadedSecretValuesFiles, encoder); err != nil { - return fmt.Errorf("error loading secret value files: %w", err) - } else { - secretsRuntimeData.decryptedSecretValues = values - secretsRuntimeData.secretValuesToMask = append(secretsRuntimeData.secretValuesToMask, secretvalues.ExtractSecretValuesFromMap(values)...) - } - } - - return nil -} - -func (secretsRuntimeData *SecretsRuntimeData) GetEncodedSecretValues( - ctx context.Context, - secretsManager *secrets_manager.SecretsManager, - secretsWorkingDir string, - noDecryptSecrets bool, -) (map[string]interface{}, error) { - if len(secretsRuntimeData.decryptedSecretValues) == 0 { - return nil, nil - } - - // FIXME: secrets encoder should receive interface{} raw data instead of []byte yaml data - - var encoder *secret.YamlEncoder - if enc, err := secretsManager.GetYamlEncoder(ctx, secretsWorkingDir, noDecryptSecrets); err != nil { - return nil, fmt.Errorf("error getting secrets yaml encoder: %w", err) - } else { - encoder = enc - } - - decryptedSecretsData, err := yaml.Marshal(secretsRuntimeData.decryptedSecretValues) - if err != nil { - return nil, fmt.Errorf("unable to marshal decrypted secrets yaml: %w", err) - } - - encryptedSecretsData, err := encoder.EncryptYamlData(decryptedSecretsData) - if err != nil { - return nil, fmt.Errorf("unable to encrypt secrets data: %w", err) - } - - var encryptedData map[string]interface{} - if err := yaml.Unmarshal(encryptedSecretsData, &encryptedData); err != nil { - return nil, fmt.Errorf("unable to unmarshal encrypted secrets data: %w", err) - } - - return encryptedData, nil -} - -func (secretsRuntimeData *SecretsRuntimeData) GetDecryptedSecretValues() map[string]interface{} { - return secretsRuntimeData.decryptedSecretValues -} - -func (secretsRuntimeData *SecretsRuntimeData) GetDecryptedSecretFilesData() map[string]string { - return secretsRuntimeData.decryptedSecretFilesData -} - -func (secretsRuntimeData *SecretsRuntimeData) GetSecretValuesToMask() []string { - return secretsRuntimeData.secretValuesToMask -} - -func LoadChartSecretValueFiles( - secretDirFiles []*werffile.ChartExtenderBufferedFile, - encoder *secret.YamlEncoder, -) (map[string]interface{}, error) { - var res map[string]interface{} - - for _, file := range secretDirFiles { - decodedData, err := encoder.DecryptYamlData(file.Data) - if err != nil { - return nil, fmt.Errorf("cannot decode file %q secret data: %w", file.Name, err) - } - - rawValues := map[string]interface{}{} - if err := yaml.Unmarshal(decodedData, &rawValues); err != nil { - return nil, fmt.Errorf("cannot unmarshal secret values file %s: %w", file.Name, err) - } - - res = CoalesceTablesFunc(rawValues, res) - } - - return res, nil -} diff --git a/pkg/helm/testdata/localhost-crt.pem b/pkg/helm/testdata/localhost-crt.pem new file mode 100644 index 00000000..70fa0a42 --- /dev/null +++ b/pkg/helm/testdata/localhost-crt.pem @@ -0,0 +1,73 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 7f:5e:fa:21:fa:ee:e4:6a:be:9b:c2:80:bf:ed:42:f3:2d:47:f5:d2 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=CO, L=Boulder, O=Helm, CN=helm.sh + Validity + Not Before: Nov 6 21:59:18 2023 GMT + Not After : Nov 3 21:59:18 2033 GMT + Subject: C=CA, ST=ON, L=Kitchener, O=Helm, CN=localhost + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public-Key: (2048 bit) + Modulus: + 00:c8:89:55:0d:0b:f1:da:e6:c0:70:7d:d3:27:cd: + b8:a8:81:8b:7c:a4:89:e5:d1:b1:78:01:1d:df:44: + 88:0b:fc:d6:81:35:3d:d1:3b:5e:8f:bb:93:b3:7e: + 28:db:ed:ff:a0:13:3a:70:a3:fe:94:6b:0b:fe:fb: + 63:00:b0:cb:dc:81:cd:80:dc:d0:2f:bf:b2:4f:9a: + 81:d4:22:dc:97:c8:8f:27:86:59:91:fa:92:05:75: + c4:cc:6b:f5:a9:6b:74:1e:f5:db:a9:f8:bf:8c:a2: + 25:fd:a0:cc:79:f4:25:57:74:a9:23:9b:e2:b7:22: + 7a:14:7a:3d:ea:f1:7e:32:6b:57:6c:2e:c6:4f:75: + 54:f9:6b:54:d2:ca:eb:54:1c:af:39:15:9b:d0:7c: + 0f:f8:55:51:04:ea:da:fa:7b:8b:63:0f:ac:39:b1: + f6:4b:8e:4e:f6:ea:e9:7b:e6:ba:5e:5a:8e:91:ef: + dc:b1:7d:52:3f:73:83:52:46:83:48:49:ff:f2:2d: + ca:54:f2:36:bb:49:cc:59:99:c0:9e:cf:8e:78:55: + 6c:ed:7d:7e:83:b8:59:2c:7d:f8:1a:81:f0:7d:f5: + 27:f2:db:ae:d4:31:54:38:fe:47:b2:ee:16:20:0f: + f1:db:2d:28:bf:6f:38:eb:11:bb:9a:d4:b2:5a:3a: + 4a:7f + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Alternative Name: + DNS:localhost + Signature Algorithm: sha256WithRSAEncryption + 47:47:fe:29:ca:94:28:75:59:ba:ab:67:ab:c6:a6:0b:0a:f2: + 0f:26:d9:1d:35:db:68:a5:d8:f5:1f:d1:87:e7:a7:74:fd:c0: + 22:aa:c8:ec:6c:d3:ac:8a:0b:ed:59:3a:a0:12:77:7c:53:74: + fd:30:59:34:8f:a4:ef:5b:98:3f:ff:cf:89:87:ed:d3:7f:41: + 2f:b1:9a:12:71:bb:fe:3a:cf:77:16:32:bc:83:90:cc:52:2f: + 3b:f4:ae:db:b1:bb:f0:dd:30:d4:03:17:5e:47:b7:06:86:7a: + 16:b1:72:2f:80:5d:d4:c0:f9:6c:91:df:5a:c5:15:86:66:68: + c8:90:8e:f1:a2:bb:40:0f:ef:26:1b:02:c4:42:de:8c:69:ec: + ad:27:d0:bc:da:7c:76:33:86:de:b7:c4:04:64:e6:f6:dc:44: + 89:7b:b8:2f:c7:28:7a:4c:a6:01:ad:a5:17:64:3a:23:da:aa: + db:ce:3f:86:e9:92:dc:0d:c4:5a:b4:52:a8:8a:ee:3d:62:7d: + b1:c8:fa:ef:96:2b:ab:f1:e1:6d:6f:7d:1e:ce:bc:7a:d0:92: + 02:1b:c8:55:36:77:bf:d4:42:d3:fc:57:ca:b7:cc:95:be:ce: + f8:6e:b2:28:ca:4d:9a:00:7d:78:c8:56:04:2e:b3:ac:03:fa: + 05:d8:42:bd +-----BEGIN CERTIFICATE----- +MIIDRDCCAiygAwIBAgIUf176Ifru5Gq+m8KAv+1C8y1H9dIwDQYJKoZIhvcNAQEL +BQAwTTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNPMRAwDgYDVQQHDAdCb3VsZGVy +MQ0wCwYDVQQKDARIZWxtMRAwDgYDVQQDDAdoZWxtLnNoMB4XDTIzMTEwNjIxNTkx +OFoXDTMzMTEwMzIxNTkxOFowUTELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMRIw +EAYDVQQHDAlLaXRjaGVuZXIxDTALBgNVBAoMBEhlbG0xEjAQBgNVBAMMCWxvY2Fs +aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMiJVQ0L8drmwHB9 +0yfNuKiBi3ykieXRsXgBHd9EiAv81oE1PdE7Xo+7k7N+KNvt/6ATOnCj/pRrC/77 +YwCwy9yBzYDc0C+/sk+agdQi3JfIjyeGWZH6kgV1xMxr9alrdB7126n4v4yiJf2g +zHn0JVd0qSOb4rciehR6PerxfjJrV2wuxk91VPlrVNLK61QcrzkVm9B8D/hVUQTq +2vp7i2MPrDmx9kuOTvbq6Xvmul5ajpHv3LF9Uj9zg1JGg0hJ//ItylTyNrtJzFmZ +wJ7PjnhVbO19foO4WSx9+BqB8H31J/LbrtQxVDj+R7LuFiAP8dstKL9vOOsRu5rU +slo6Sn8CAwEAAaMYMBYwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEB +CwUAA4IBAQBHR/4pypQodVm6q2erxqYLCvIPJtkdNdtopdj1H9GH56d0/cAiqsjs +bNOsigvtWTqgEnd8U3T9MFk0j6TvW5g//8+Jh+3Tf0EvsZoScbv+Os93FjK8g5DM +Ui879K7bsbvw3TDUAxdeR7cGhnoWsXIvgF3UwPlskd9axRWGZmjIkI7xortAD+8m +GwLEQt6MaeytJ9C82nx2M4bet8QEZOb23ESJe7gvxyh6TKYBraUXZDoj2qrbzj+G +6ZLcDcRatFKoiu49Yn2xyPrvliur8eFtb30ezrx60JICG8hVNne/1ELT/FfKt8yV +vs74brIoyk2aAH14yFYELrOsA/oF2EK9 +-----END CERTIFICATE----- diff --git a/pkg/helm/testdata/openssl.conf b/pkg/helm/testdata/openssl.conf index 9b27e445..be5ff04b 100644 --- a/pkg/helm/testdata/openssl.conf +++ b/pkg/helm/testdata/openssl.conf @@ -40,3 +40,7 @@ subjectAltName = @alternate_names [alternate_names] DNS.1 = helm.sh IP.1 = 127.0.0.1 + +# # Used to generate localhost-crt.pem +# [alternate_names] +# DNS.1 = localhost diff --git a/pkg/kube/config.go b/pkg/kube/config.go index 512aea42..7eacc763 100644 --- a/pkg/kube/config.go +++ b/pkg/kube/config.go @@ -21,7 +21,7 @@ type KubeConfig struct { RestConfig *rest.Config } -func NewKubeConfig(ctx context.Context, kubeConfigPaths []string, opts KubeConfigOptions) (*KubeConfig, error) { +func NewKubeConfig(ctx context.Context, opts KubeConfigOptions) (*KubeConfig, error) { var authProviderConfig *api.AuthProviderConfig if opts.KubeAuthProviderName != "" || len(opts.KubeAuthProviderConfig) != 0 { authProviderConfig = &api.AuthProviderConfig{ @@ -72,10 +72,15 @@ func NewKubeConfig(ctx context.Context, kubeConfigPaths []string, opts KubeConfi clientConfig = clientcmd.NewDefaultClientConfig(*config, overrides) } else { - loadingRules := &clientcmd.ClientConfigLoadingRules{ - Precedence: kubeConfigPaths, - MigrationRules: clientcmd.NewDefaultClientConfigLoadingRules().MigrationRules, - DefaultClientConfig: &clientcmd.DefaultClientConfig, + var loadingRules *clientcmd.ClientConfigLoadingRules + if len(opts.KubeConfigPaths) > 0 { + loadingRules = &clientcmd.ClientConfigLoadingRules{ + Precedence: opts.KubeConfigPaths, + MigrationRules: clientcmd.NewDefaultClientConfigLoadingRules().MigrationRules, + DefaultClientConfig: &clientcmd.DefaultClientConfig, + } + } else { + loadingRules = clientcmd.NewDefaultClientConfigLoadingRules() } clientConfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) diff --git a/pkg/kube/fake/client_dynamic.go b/pkg/kube/fake/client_dynamic.go index 88b41103..fd4287a0 100644 --- a/pkg/kube/fake/client_dynamic.go +++ b/pkg/kube/fake/client_dynamic.go @@ -1,6 +1,7 @@ package fake import ( + "encoding/json" "fmt" "reflect" @@ -9,7 +10,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/json" dynamicfake "k8s.io/client-go/dynamic/fake" staticfake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" diff --git a/pkg/legacy/deploy/resources_waiter.go b/pkg/legacy/deploy/resources_waiter.go deleted file mode 100644 index ff6ba439..00000000 --- a/pkg/legacy/deploy/resources_waiter.go +++ /dev/null @@ -1,528 +0,0 @@ -package deploy - -import ( - "context" - "fmt" - "math" - "os" - "regexp" - "strconv" - "strings" - "time" - - flaggerv1beta1 "github.com/fluxcd/flagger/pkg/apis/flagger/v1beta1" - flaggerscheme "github.com/fluxcd/flagger/pkg/client/clientset/versioned/scheme" - appsv1 "k8s.io/api/apps/v1" - appsv1beta1 "k8s.io/api/apps/v1beta1" - appsv1beta2 "k8s.io/api/apps/v1beta2" - batchv1 "k8s.io/api/batch/v1" - extensions "k8s.io/api/extensions/v1beta1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/cli-runtime/pkg/resource" - "k8s.io/client-go/kubernetes/scheme" - - kdkube "github.com/werf/kubedog/pkg/kube" - "github.com/werf/kubedog/pkg/tracker" - "github.com/werf/kubedog/pkg/tracker/resid" - "github.com/werf/kubedog/pkg/trackers/elimination" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack/generic" - "github.com/werf/logboek" - helm_kube "github.com/werf/nelm/pkg/helm/pkg/kube" -) - -func init() { - flaggerscheme.AddToScheme(scheme.Scheme) -} - -const ( - ExternalDependencyNamespaceAnnoName = "external-dependency.werf.io/namespace" - ExternalDependencyResourceAnnoName = "external-dependency.werf.io/resource" - FailModeAnnoName = "werf.io/fail-mode" - FailuresAllowedPerReplicaAnnoName = "werf.io/failures-allowed-per-replica" - IgnoreReadinessProbeFailsForPrefix = "werf.io/ignore-readiness-probe-fails-for-" - LogRegexAnnoName = "werf.io/log-regex" - LogRegexForAnnoPrefix = "werf.io/log-regex-for-" - NoActivityTimeoutName = "werf.io/no-activity-timeout" - ReplicasOnCreationAnnoName = "werf.io/replicas-on-creation" - ShowEventsAnnoName = "werf.io/show-service-messages" - ShowLogsOnlyForContainers = "werf.io/show-logs-only-for-containers" - ShowLogsUntilAnnoName = "werf.io/show-logs-until" - SkipLogsAnnoName = "werf.io/skip-logs" - SkipLogsForContainersAnnoName = "werf.io/skip-logs-for-containers" - StageWeightAnnoName = "werf.io/weight" - TrackTerminationModeAnnoName = "werf.io/track-termination-mode" -) - -// TODO(major): get rid -type ResourcesWaiter struct { - Client *helm_kube.Client - HooksStatusProgressPeriod time.Duration - LogsFromTime time.Time - StatusProgressPeriod time.Duration -} - -func NewResourcesWaiter(client *helm_kube.Client, logsFromTime time.Time, statusProgressPeriod, hooksStatusProgressPeriod time.Duration) *ResourcesWaiter { - return &ResourcesWaiter{ - Client: client, - HooksStatusProgressPeriod: hooksStatusProgressPeriod, - LogsFromTime: logsFromTime, - StatusProgressPeriod: statusProgressPeriod, - } -} - -func (waiter *ResourcesWaiter) Wait(ctx context.Context, resources helm_kube.ResourceList, timeout time.Duration) error { - if os.Getenv("WERF_DISABLE_RESOURCES_WAITER") == "1" { - return nil - } - - specs, err := makeMultitrackSpecsFromResList(ctx, resources, timeout, waiter.StatusProgressPeriod) - if err != nil { - return fmt.Errorf("error making multitrack specs: %w", err) - } - - // NOTE: use context from resources-waiter object here, will be changed in helm 3 - logboek.Context(ctx).LogOptionalLn() - - return logboek.Context(ctx).LogProcess("Waiting for resources to become ready"). - DoError(func() error { - return multitrack.Multitrack(kdkube.Client, *specs, multitrack.MultitrackOptions{ - StatusProgressPeriod: waiter.StatusProgressPeriod, - Options: tracker.Options{ - Timeout: timeout, - LogsFromTime: waiter.LogsFromTime, - }, - DynamicClient: kdkube.DynamicClient, - DiscoveryClient: kdkube.CachedDiscoveryClient, - Mapper: kdkube.Mapper, - }) - }) -} - -func (waiter *ResourcesWaiter) WaitUntilDeleted(ctx context.Context, specs []*helm_kube.ResourcesWaiterDeleteResourceSpec, timeout time.Duration) error { - if len(specs) == 0 { - return nil - } - - var eliminationSpecs []*elimination.EliminationTrackerSpec - for _, spec := range specs { - eliminationSpecs = append(eliminationSpecs, &elimination.EliminationTrackerSpec{ - ResourceName: spec.ResourceName, - Namespace: spec.Namespace, - GroupVersionResource: spec.GroupVersionResource, - }) - } - - var resourcesDescParts []string - for _, spec := range specs { - resourcesDescParts = append(resourcesDescParts, fmt.Sprintf("%s/%s", strings.ToLower(spec.GroupVersionResource.Resource), spec.ResourceName)) - } - - return logboek.Context(ctx).Default().LogProcess("Waiting for resources elimination: %s", strings.Join(resourcesDescParts, ", ")).DoError(func() error { - return elimination.TrackUntilEliminated(ctx, kdkube.DynamicClient, eliminationSpecs, elimination.EliminationTrackerOptions{Timeout: timeout, StatusProgressPeriod: waiter.StatusProgressPeriod}) - }) -} - -func (waiter *ResourcesWaiter) WatchUntilReady(ctx context.Context, resources helm_kube.ResourceList, timeout time.Duration) error { - if os.Getenv("WERF_DISABLE_RESOURCES_WAITER") == "1" { - return nil - } - - specs, err := makeMultitrackSpecsFromResList(ctx, resources, timeout, waiter.HooksStatusProgressPeriod) - if err != nil { - return fmt.Errorf("error making multitrack specs: %w", err) - } - - // NOTE: use context from resources-waiter object here, will be changed in helm 3 - logboek.Context(ctx).LogOptionalLn() - - return logboek.Context(ctx).LogProcess("Waiting for helm hooks termination"). - DoError(func() error { - return multitrack.Multitrack(kdkube.Client, *specs, multitrack.MultitrackOptions{ - StatusProgressPeriod: waiter.HooksStatusProgressPeriod, - Options: tracker.Options{ - Timeout: timeout, - LogsFromTime: waiter.LogsFromTime, - }, - DynamicClient: kdkube.DynamicClient, - DiscoveryClient: kdkube.CachedDiscoveryClient, - Mapper: kdkube.Mapper, - }) - }) -} - -type allowedFailuresCountOptions struct { - defaultPerReplica int - multiplier int -} - -func makeMultitrackSpecsFromResList(ctx context.Context, resources helm_kube.ResourceList, timeout, statusProgressPeriod time.Duration) (*multitrack.MultitrackSpecs, error) { - specs := &multitrack.MultitrackSpecs{} - - for _, v := range resources { - switch value := asVersioned(v).(type) { - case *appsv1.Deployment: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "deploy") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.Deployments = append(specs.Deployments, *spec) - } - case *appsv1beta1.Deployment: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "deploy") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.Deployments = append(specs.Deployments, *spec) - } - case *appsv1beta2.Deployment: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "deploy") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.Deployments = append(specs.Deployments, *spec) - } - case *extensions.Deployment: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "deploy") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.Deployments = append(specs.Deployments, *spec) - } - case *extensions.DaemonSet: - // TODO: multiplier equals 3 because typically there are only 3 nodes in the cluster. - // TODO: It is better to fetch number of nodes dynamically, but in the most cases multiplier=3 will work ok. - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: 3}, "ds") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.DaemonSets = append(specs.DaemonSets, *spec) - } - case *appsv1.DaemonSet: - // TODO: multiplier equals 3 because typically there are only 3 nodes in the cluster. - // TODO: It is better to fetch number of nodes dynamically, but in the most cases multiplier=3 will work ok. - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: 3}, "ds") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.DaemonSets = append(specs.DaemonSets, *spec) - } - case *appsv1beta2.DaemonSet: - // TODO: multiplier equals 3 because typically there are only 3 nodes in the cluster. - // TODO: It is better to fetch number of nodes dynamically, but in the most cases multiplier=3 will work ok. - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: 3}, "ds") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.DaemonSets = append(specs.DaemonSets, *spec) - } - case *appsv1.StatefulSet: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "sts") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.StatefulSets = append(specs.StatefulSets, *spec) - } - case *appsv1beta1.StatefulSet: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "sts") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.StatefulSets = append(specs.StatefulSets, *spec) - } - case *appsv1beta2.StatefulSet: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 1, multiplier: extractSpecReplicas(value.Spec.Replicas)}, "sts") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.StatefulSets = append(specs.StatefulSets, *spec) - } - case *batchv1.Job: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 0, multiplier: 1}, "job") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.Jobs = append(specs.Jobs, *spec) - } - case *flaggerv1beta1.Canary: - spec, err := makeMultitrackSpec(ctx, &value.ObjectMeta, allowedFailuresCountOptions{defaultPerReplica: 0, multiplier: 1}, "canary") - if err != nil { - return nil, fmt.Errorf("cannot track %s %s: %w", value.Kind, value.Name, err) - } - if spec != nil { - specs.Canaries = append(specs.Canaries, *spec) - } - default: - obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(v.Object) - if err != nil { - return nil, fmt.Errorf("error converting object to unstructured: %w", err) - } - - object := unstructured.Unstructured{ - Object: obj, - } - - resourceID := resid.NewResourceID(object.GetName(), object.GroupVersionKind(), resid.NewResourceIDOptions{ - Namespace: object.GetNamespace(), - }) - - if spec, err := makeGenericSpec(ctx, resourceID, statusProgressPeriod, timeout, object.GetAnnotations()); err != nil { - logboek.Context(ctx).Warn().LogLn() - logboek.Context(ctx).Warn().LogF("WARNING %s\n", err) - } else if spec != nil { - specs.Generics = append(specs.Generics, spec) - } - } - } - - return specs, nil -} - -func makeMultitrackSpec(ctx context.Context, objMeta *metav1.ObjectMeta, failuresCountOptions allowedFailuresCountOptions, kind string) (*multitrack.MultitrackSpec, error) { - multitrackSpec, err := prepareMultitrackSpec(objMeta.Name, kind, objMeta.Namespace, objMeta.Annotations, failuresCountOptions) - if err != nil { - logboek.Context(ctx).Warn().LogLn() - logboek.Context(ctx).Warn().LogF("WARNING %s\n", err) - - return nil, nil - } - - return multitrackSpec, nil -} - -func prepareMultitrackSpec(metadataName, resourceNameOrKind, namespace string, annotations map[string]string, failuresCountOptions allowedFailuresCountOptions) (*multitrack.MultitrackSpec, error) { - defaultAllowFailuresCount := new(int) - - // Allow 1 fail per replica by default - *defaultAllowFailuresCount = applyAllowedFailuresCountMultiplier(failuresCountOptions.defaultPerReplica, failuresCountOptions.multiplier) - - multitrackSpec := &multitrack.MultitrackSpec{ - ResourceName: metadataName, - Namespace: namespace, - LogRegexByContainerName: map[string]*regexp.Regexp{}, - AllowFailuresCount: defaultAllowFailuresCount, - IgnoreReadinessProbeFailsByContainerName: map[string]time.Duration{}, - } - -mainLoop: - for annoName, annoValue := range annotations { - invalidAnnoValueError := fmt.Errorf("%s/%s annotation %s with invalid value %s", resourceNameOrKind, metadataName, annoName, annoValue) - - switch annoName { - case ShowLogsUntilAnnoName: - return nil, fmt.Errorf("%s/%s annotation %s not supported yet", resourceNameOrKind, metadataName, annoName) - case SkipLogsAnnoName: - boolValue, err := strconv.ParseBool(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: bool expected: %w", invalidAnnoValueError.Error(), err) - } - - multitrackSpec.SkipLogs = boolValue - case ShowEventsAnnoName: - boolValue, err := strconv.ParseBool(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: bool expected: %w", invalidAnnoValueError.Error(), err) - } - - multitrackSpec.ShowServiceMessages = boolValue - case TrackTerminationModeAnnoName: - trackTerminationModeValue := multitrack.TrackTerminationMode(annoValue) - values := []multitrack.TrackTerminationMode{multitrack.WaitUntilResourceReady, multitrack.NonBlocking} - for _, value := range values { - if value == trackTerminationModeValue { - multitrackSpec.TrackTerminationMode = trackTerminationModeValue - continue mainLoop - } - } - - return nil, fmt.Errorf("%w: choose one of %v", invalidAnnoValueError, values) - case FailModeAnnoName: - failModeValue := multitrack.FailMode(annoValue) - values := []multitrack.FailMode{multitrack.IgnoreAndContinueDeployProcess, multitrack.FailWholeDeployProcessImmediately, multitrack.LegacyHopeUntilEndOfDeployProcess} - for _, value := range values { - if value == failModeValue { - multitrackSpec.FailMode = failModeValue - continue mainLoop - } - } - - return nil, fmt.Errorf("%w: choose one of %v", invalidAnnoValueError, values) - case FailuresAllowedPerReplicaAnnoName: - intValue, err := strconv.Atoi(annoValue) - if err != nil || intValue < 0 { - return nil, fmt.Errorf("%w: positive or zero integer expected", invalidAnnoValueError) - } - - allowFailuresCount := new(int) - *allowFailuresCount = applyAllowedFailuresCountMultiplier(intValue, failuresCountOptions.multiplier) - multitrackSpec.AllowFailuresCount = allowFailuresCount - case LogRegexAnnoName: - regexpValue, err := regexp.Compile(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: %w", invalidAnnoValueError.Error(), err) - } - - multitrackSpec.LogRegex = regexpValue - // case ShowLogsUntilAnnoName: - // deployConditionValue := multitrack.DeployCondition(annoValue) - // values := []multitrack.DeployCondition{multitrack.ControllerIsReady, multitrack.PodIsReady, multitrack.EndOfDeploy} - // for _, value := range values { - // if value == deployConditionValue { - // multitrackSpec.ShowLogsUntil = deployConditionValue - // continue mainLoop - // } - // } - - // return nil, fmt.Errorf("%s: choose one of %v", invalidAnnoValueError, values) - case SkipLogsForContainersAnnoName: - var containerNames []string - for _, v := range strings.Split(annoValue, ",") { - containerName := strings.TrimSpace(v) - if containerName == "" { - return nil, fmt.Errorf("%w: containers names separated by comma expected", invalidAnnoValueError) - } - - containerNames = append(containerNames, containerName) - } - - multitrackSpec.SkipLogsForContainers = containerNames - case ShowLogsOnlyForContainers: - var containerNames []string - for _, v := range strings.Split(annoValue, ",") { - containerName := strings.TrimSpace(v) - if containerName == "" { - return nil, fmt.Errorf("%w: containers names separated by comma expected", invalidAnnoValueError) - } - - containerNames = append(containerNames, containerName) - } - - multitrackSpec.ShowLogsOnlyForContainers = containerNames - default: - if strings.HasPrefix(annoName, LogRegexForAnnoPrefix) { - if containerName := strings.TrimPrefix(annoName, LogRegexForAnnoPrefix); containerName != "" { - regexpValue, err := regexp.Compile(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: %w", invalidAnnoValueError.Error(), err) - } - - multitrackSpec.LogRegexByContainerName[containerName] = regexpValue - } - } - if strings.HasPrefix(annoName, IgnoreReadinessProbeFailsForPrefix) { - if containerName := strings.TrimPrefix(annoName, IgnoreReadinessProbeFailsForPrefix); containerName != "" { - ignoreDuration, err := time.ParseDuration(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: %w", invalidAnnoValueError.Error(), err) - } - if math.Signbit(ignoreDuration.Seconds()) { - return nil, fmt.Errorf("%w: can't be less than 0", invalidAnnoValueError) - } - - multitrackSpec.IgnoreReadinessProbeFailsByContainerName[containerName] = ignoreDuration - } - } - } - } - - return multitrackSpec, nil -} - -func applyAllowedFailuresCountMultiplier(value, multiplier int) int { - if multiplier > 0 { - return value * multiplier - } - - return value -} - -func asVersioned(info *resource.Info) runtime.Object { - convertor := runtime.ObjectConvertor(scheme.Scheme) - groupVersioner := runtime.GroupVersioner(schema.GroupVersions(scheme.Scheme.PrioritizedVersionsAllGroups())) - if info.Mapping != nil { - groupVersioner = info.Mapping.GroupVersionKind.GroupVersion() - } - if obj, err := convertor.ConvertToVersion(info.Object, groupVersioner); err == nil { - return obj - } - - return info.Object -} - -func extractSpecReplicas(specReplicas *int32) int { - if specReplicas != nil { - return int(*specReplicas) - } - - return 1 -} - -func makeGenericSpec(ctx context.Context, resID *resid.ResourceID, statusProgressPeriod, timeout time.Duration, annotations map[string]string) (*generic.Spec, error) { - genericSpec := &generic.Spec{ - ResourceID: resID, - Timeout: timeout, - StatusProgressPeriod: statusProgressPeriod, - } - -mainLoop: - for annoName, annoValue := range annotations { - invalidAnnoValueError := fmt.Errorf("%s annotation %s with invalid value %s", resID, annoName, annoValue) - - switch annoName { - case ShowEventsAnnoName: - boolValue, err := strconv.ParseBool(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: bool expected: %w", invalidAnnoValueError.Error(), err) - } - - genericSpec.ShowServiceMessages = boolValue - case TrackTerminationModeAnnoName: - trackTerminationModeValue := generic.TrackTerminationMode(annoValue) - values := []generic.TrackTerminationMode{generic.WaitUntilResourceReady, generic.NonBlocking} - for _, value := range values { - if value == trackTerminationModeValue { - genericSpec.TrackTerminationMode = trackTerminationModeValue - continue mainLoop - } - } - - return nil, fmt.Errorf("%w: choose one of %v", invalidAnnoValueError, values) - case FailModeAnnoName: - failModeValue := generic.FailMode(annoValue) - values := []generic.FailMode{generic.IgnoreAndContinueDeployProcess, generic.FailWholeDeployProcessImmediately, generic.HopeUntilEndOfDeployProcess} - for _, value := range values { - if value == failModeValue { - genericSpec.FailMode = failModeValue - continue mainLoop - } - } - - return nil, fmt.Errorf("%w: choose one of %v", invalidAnnoValueError, values) - case NoActivityTimeoutName: - noActivityTimeout, err := time.ParseDuration(annoValue) - if err != nil { - return nil, fmt.Errorf("%s: %w", invalidAnnoValueError.Error(), err) - } else if noActivityTimeout.Seconds() < 1 { - return nil, fmt.Errorf("%w: can't be less than 1 second", invalidAnnoValueError) - } - - genericSpec.NoActivityTimeout = &noActivityTimeout - } - } - - return genericSpec, nil -} diff --git a/pkg/legacy/deploy/stages_splitter.go b/pkg/legacy/deploy/stages_splitter.go deleted file mode 100644 index 4aebefd6..00000000 --- a/pkg/legacy/deploy/stages_splitter.go +++ /dev/null @@ -1,64 +0,0 @@ -package deploy - -import ( - "fmt" - "sort" - "strconv" - - "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/cli-runtime/pkg/resource" - - "github.com/werf/nelm/pkg/helm/pkg/kube" - "github.com/werf/nelm/pkg/helm/pkg/phases/stages" -) - -var metadataAccessor = meta.NewAccessor() - -// TODO(major): get rid -type StagesSplitter struct{} - -func NewStagesSplitter() *StagesSplitter { - return &StagesSplitter{} -} - -func (s *StagesSplitter) Split(resources kube.ResourceList) (stages.SortedStageList, error) { - stageList := stages.SortedStageList{} - - if err := resources.Visit(func(resInfo *resource.Info, err error) error { - if err != nil { - return err - } - - annotations, err := metadataAccessor.Annotations(resInfo.Object) - if err != nil { - return fmt.Errorf("error getting annotations for object: %w", err) - } - - var weight int - if w, ok := annotations[StageWeightAnnoName]; ok { - weight, err = strconv.Atoi(w) - if err != nil { - return fmt.Errorf("error parsing annotation \"%s: %s\" — value should be an integer: %w", StageWeightAnnoName, w, err) - } - } - - stage := stageList.StageByWeight(weight) - - if stage == nil { - stage = &stages.Stage{ - Weight: weight, - } - stageList = append(stageList, stage) - } - - stage.DesiredResources.Append(resInfo) - - return nil - }); err != nil { - return nil, fmt.Errorf("error visiting resources list: %w", err) - } - - sort.Sort(stageList) - - return stageList, nil -} diff --git a/pkg/legacy/secret/chart_secrets_loader.go b/pkg/legacy/secret/chart_secrets_loader.go new file mode 100644 index 00000000..f9f80cae --- /dev/null +++ b/pkg/legacy/secret/chart_secrets_loader.go @@ -0,0 +1,60 @@ +package secret + +import ( + "fmt" + "path/filepath" + "strings" + "unicode" + + "github.com/werf/common-go/pkg/secret" + "github.com/werf/common-go/pkg/util" + "github.com/werf/nelm/pkg/common" +) + +const ( + DefaultSecretValuesFileName = "secret-values.yaml" + SecretDirName = "secret" +) + +func GetDefaultSecretValuesFile(loadedChartFiles []*common.BufferedFile) *common.BufferedFile { + for _, f := range loadedChartFiles { + if f.Name == DefaultSecretValuesFileName { + return f + } + } + + return nil +} + +func GetSecretDirFiles(loadedChartFiles []*common.BufferedFile) []*common.BufferedFile { + var res []*common.BufferedFile + + for _, f := range loadedChartFiles { + if !util.IsSubpathOfBasePath(SecretDirName, f.Name) { + continue + } + res = append(res, f) + } + + return res +} + +func LoadChartSecretDirFilesData(secretFiles []*common.BufferedFile, encoder *secret.YamlEncoder) (map[string]string, error) { + res := make(map[string]string) + + for _, f := range secretFiles { + if !util.IsSubpathOfBasePath(SecretDirName, f.Name) { + continue + } + + decodedData, err := encoder.Decrypt([]byte(strings.TrimRightFunc(string(f.Data), unicode.IsSpace))) + if err != nil { + return nil, fmt.Errorf("decoding %s: %w", f.Name, err) + } + + relPath := util.GetRelativeToBaseFilepath(SecretDirName, f.Name) + res[filepath.ToSlash(relPath)] = string(decodedData) + } + + return res, nil +} diff --git a/pkg/legacy/secret/runtime_data.go b/pkg/legacy/secret/runtime_data.go new file mode 100644 index 00000000..d5062fac --- /dev/null +++ b/pkg/legacy/secret/runtime_data.go @@ -0,0 +1,151 @@ +package secret + +import ( + "context" + "fmt" + "os" + + "sigs.k8s.io/yaml" + + "github.com/werf/common-go/pkg/secret" + "github.com/werf/common-go/pkg/secrets_manager" + "github.com/werf/nelm/pkg/common" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" +) + +var _ RuntimeData = (*SecretsRuntimeData)(nil) + +type DecodeAndLoadSecretsOptions = chartcommon.DecodeAndLoadSecretsOptions + +type RuntimeData interface { + DecodeAndLoadSecrets(ctx context.Context, loadedChartFiles []*common.BufferedFile, secretsManager *secrets_manager.SecretsManager, opts DecodeAndLoadSecretsOptions) error + GetEncodedSecretValues(ctx context.Context, secretsManager *secrets_manager.SecretsManager, secretsWorkingDir string, noDecryptSecrets bool) (map[string]interface{}, error) + GetDecryptedSecretValues() map[string]interface{} + GetDecryptedSecretFilesData() map[string]string +} + +type SecretsRuntimeData struct { + decryptedSecretFilesData map[string]string + decryptedSecretValues map[string]interface{} +} + +func NewSecretsRuntimeData() *SecretsRuntimeData { + return &SecretsRuntimeData{ + decryptedSecretFilesData: make(map[string]string), + } +} + +func (s *SecretsRuntimeData) DecodeAndLoadSecrets(ctx context.Context, loadedChartFiles []*common.BufferedFile, secretsManager *secrets_manager.SecretsManager, opts DecodeAndLoadSecretsOptions) error { + secretDirFiles := GetSecretDirFiles(loadedChartFiles) + + var loadedSecretValuesFiles []*common.BufferedFile + + if !opts.WithoutDefaultSecretValues { + if defaultSecretValues := GetDefaultSecretValuesFile(loadedChartFiles); defaultSecretValues != nil { + loadedSecretValuesFiles = append(loadedSecretValuesFiles, defaultSecretValues) + } + } + + for _, customSecretValuesFileName := range opts.CustomSecretValueFiles { + f := &common.BufferedFile{Name: customSecretValuesFileName} + + if opts.LoadFromLocalFilesystem { + data, err := os.ReadFile(customSecretValuesFileName) + if err != nil { + return fmt.Errorf("read custom secret values file %q from local filesystem: %w", customSecretValuesFileName, err) + } + f.Data = data + } else { + data, err := common.ChartFileReader.ReadChartFile(ctx, customSecretValuesFileName) + if err != nil { + return fmt.Errorf("read custom secret values file %q: %w", customSecretValuesFileName, err) + } + f.Data = data + } + + loadedSecretValuesFiles = append(loadedSecretValuesFiles, f) + } + + var encoder *secret.YamlEncoder + if len(secretDirFiles)+len(loadedSecretValuesFiles) > 0 { + enc, err := secretsManager.GetYamlEncoder(ctx, opts.SecretsWorkingDir, opts.NoDecryptSecrets) + if err != nil { + return fmt.Errorf("get secrets yaml encoder: %w", err) + } + encoder = enc + } + + if len(secretDirFiles) > 0 { + data, err := LoadChartSecretDirFilesData(secretDirFiles, encoder) + if err != nil { + return fmt.Errorf("load secret files data: %w", err) + } + s.decryptedSecretFilesData = data + } + + if len(loadedSecretValuesFiles) > 0 { + values, err := LoadChartSecretValueFiles(loadedSecretValuesFiles, encoder) + if err != nil { + return fmt.Errorf("load secret value files: %w", err) + } + s.decryptedSecretValues = values + } + + return nil +} + +func (s *SecretsRuntimeData) GetDecryptedSecretFilesData() map[string]string { + return s.decryptedSecretFilesData +} + +func (s *SecretsRuntimeData) GetDecryptedSecretValues() map[string]interface{} { + return s.decryptedSecretValues +} + +func (s *SecretsRuntimeData) GetEncodedSecretValues(ctx context.Context, secretsManager *secrets_manager.SecretsManager, secretsWorkingDir string, noDecryptSecrets bool) (map[string]interface{}, error) { + if len(s.decryptedSecretValues) == 0 { + return nil, nil + } + + enc, err := secretsManager.GetYamlEncoder(ctx, secretsWorkingDir, noDecryptSecrets) + if err != nil { + return nil, fmt.Errorf("get secrets yaml encoder: %w", err) + } + + decryptedSecretsData, err := yaml.Marshal(s.decryptedSecretValues) + if err != nil { + return nil, fmt.Errorf("marshal decrypted secrets yaml: %w", err) + } + + encryptedSecretsData, err := enc.EncryptYamlData(decryptedSecretsData) + if err != nil { + return nil, fmt.Errorf("encrypt secrets data: %w", err) + } + + var encryptedData map[string]interface{} + if err := yaml.Unmarshal(encryptedSecretsData, &encryptedData); err != nil { + return nil, fmt.Errorf("unmarshal encrypted secrets data: %w", err) + } + + return encryptedData, nil +} + +func LoadChartSecretValueFiles(secretDirFiles []*common.BufferedFile, encoder *secret.YamlEncoder) (map[string]interface{}, error) { + var res map[string]interface{} + + for _, f := range secretDirFiles { + decodedData, err := encoder.DecryptYamlData(f.Data) + if err != nil { + return nil, fmt.Errorf("decode file %q secret data: %w", f.Name, err) + } + + rawValues := map[string]interface{}{} + if err := yaml.Unmarshal(decodedData, &rawValues); err != nil { + return nil, fmt.Errorf("unmarshal secret values file %s: %w", f.Name, err) + } + + res = common.LegacyCoalesceTablesFunc(rawValues, res) + } + + return res, nil +} diff --git a/pkg/legacy/secret/template_funcs.go b/pkg/legacy/secret/template_funcs.go new file mode 100644 index 00000000..de1d8653 --- /dev/null +++ b/pkg/legacy/secret/template_funcs.go @@ -0,0 +1,40 @@ +package secret + +import ( + "fmt" + "path" + "strings" + "text/template" +) + +type SecretFilesRuntimeData interface { + GetDecryptedSecretFilesData() map[string]string +} + +func FuncMap(secretsRuntimeData SecretFilesRuntimeData) template.FuncMap { + funcMap := template.FuncMap{} + SetupWerfSecretFile(secretsRuntimeData, funcMap) + + return funcMap +} + +func SetupWerfSecretFile(secretsRuntimeData SecretFilesRuntimeData, funcMap template.FuncMap) { + funcMap["werf_secret_file"] = func(secretRelativePath string) (string, error) { + if path.IsAbs(secretRelativePath) { + return "", fmt.Errorf("expected relative secret file path, given path %v", secretRelativePath) + } + + decodedData, ok := secretsRuntimeData.GetDecryptedSecretFilesData()[secretRelativePath] + + if !ok { + var secretFiles []string + for key := range secretsRuntimeData.GetDecryptedSecretFilesData() { + secretFiles = append(secretFiles, key) + } + + return "", fmt.Errorf("secret file %q not found, you may use one of the following: %q", secretRelativePath, strings.Join(secretFiles, "', '")) + } + + return decodedData, nil + } +} diff --git a/pkg/log/common.go b/pkg/log/common.go new file mode 100644 index 00000000..ae9264f2 --- /dev/null +++ b/pkg/log/common.go @@ -0,0 +1,3 @@ +package log + +var Default Logger = NewLogboekLogger() diff --git a/pkg/log/init.go b/pkg/log/init.go deleted file mode 100644 index 71a3bef3..00000000 --- a/pkg/log/init.go +++ /dev/null @@ -1,182 +0,0 @@ -package log - -import ( - "context" - "flag" - "fmt" - "io" - stdlog "log" - "os" - - cdlog "github.com/containerd/log" - "github.com/davecgh/go-spew/spew" - "github.com/gookit/color" - "github.com/hofstadter-io/cinful" - "github.com/samber/lo" - "github.com/sirupsen/logrus" - "github.com/xo/terminfo" - "k8s.io/klog" - klogv2 "k8s.io/klog/v2" - - "github.com/werf/kubedog/pkg/tracker/debug" - "github.com/werf/logboek" - "github.com/werf/nelm/pkg/helm/pkg/engine" -) - -var Default Logger = NewLogboekLogger() - -type SetupLoggingOptions struct { - ColorMode string - LogIsParseable bool -} - -// Sets up logging levels, colors, output formats, etc. -func SetupLogging(ctx context.Context, logLevel Level, opts SetupLoggingOptions) context.Context { - if val := ctx.Value(LogboekLoggerCtxKeyName); val == nil { - ctx = logboek.NewContext(ctx, logboek.DefaultLogger()) - } - - Default.SetLevel(ctx, logLevel) - - spew.Config.DisablePointerAddresses = true - spew.Config.DisableCapacities = true - - switch logLevel { - case SilentLevel, ErrorLevel, WarningLevel, InfoLevel: - stdlog.SetOutput(io.Discard) - - klog.SetOutput(io.Discard) - // From: https://github.com/kubernetes/klog/issues/87#issuecomment-1671820147 - klogFlags := &flag.FlagSet{} - klog.InitFlags(klogFlags) - lo.Must0(klogFlags.Set("logtostderr", "false")) - lo.Must0(klogFlags.Set("alsologtostderr", "false")) - lo.Must0(klogFlags.Set("stderrthreshold", "4")) - - klogv2.SetOutput(io.Discard) - // From: https://github.com/kubernetes/klog/issues/87#issuecomment-1671820147 - klogV2Flags := &flag.FlagSet{} - klogv2.InitFlags(klogV2Flags) - lo.Must0(klogV2Flags.Set("logtostderr", "false")) - lo.Must0(klogV2Flags.Set("alsologtostderr", "false")) - lo.Must0(klogV2Flags.Set("stderrthreshold", "4")) - - logrus.SetOutput(io.Discard) - - cdlog.L.Logger.SetOutput(io.Discard) - - engine.Debug = false - - debug.SetDebug(false) - case DebugLevel: - stdlog.SetOutput(os.Stdout) - - klog.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) - klog.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) - klog.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) - klog.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) - - klogv2.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) - klogv2.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) - klogv2.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) - klogv2.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) - - logrus.SetOutput(logboek.Context(ctx).OutStream()) - logrus.SetLevel(logrus.DebugLevel) - - cdlog.L.Logger.SetOutput(logboek.Context(ctx).OutStream()) - cdlog.L.Logger.SetLevel(logrus.DebugLevel) - - engine.Debug = true - - debug.SetDebug(true) - case TraceLevel: - stdlog.SetOutput(os.Stdout) - - klog.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) - klog.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) - klog.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) - klog.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) - - klogv2.SetOutputBySeverity("FATAL", logboek.Context(ctx).ErrStream()) - klogv2.SetOutputBySeverity("ERROR", logboek.Context(ctx).ErrStream()) - klogv2.SetOutputBySeverity("WARNING", logboek.Context(ctx).ErrStream()) - klogv2.SetOutputBySeverity("INFO", logboek.Context(ctx).OutStream()) - - logrus.SetOutput(logboek.Context(ctx).OutStream()) - logrus.SetLevel(logrus.TraceLevel) - - cdlog.L.Logger.SetOutput(logboek.Context(ctx).OutStream()) - cdlog.L.Logger.SetLevel(logrus.TraceLevel) - - engine.Debug = true - - debug.SetDebug(true) - default: - panic(fmt.Sprintf("unknown log level %q", logLevel)) - } - - colorLevel := getColorLevel(opts.ColorMode, opts.LogIsParseable) - - color.Enable = colorLevel != terminfo.ColorLevelNone - color.ForceSetColorLevel(colorLevel) - - return ctx -} - -func getColorLevel(mode string, logIsParseable bool) terminfo.ColorLevel { - switch mode { - case LogColorModeOff: - return terminfo.ColorLevelNone - case LogColorModeOn: - if colorLevel := color.DetectColorLevel(); colorLevel == terminfo.ColorLevelNone { - return terminfo.ColorLevelHundreds - } else { - return colorLevel - } - } - - if ciInfo := cinful.Info(); ciInfo != nil { - switch ciInfo.Constant { - case "GITLAB", "GITHUB_ACTIONS": - if logIsParseable { - return terminfo.ColorLevelNone - } else { - return terminfo.ColorLevelHundreds - } - case "JENKINS": - if logIsParseable { - return terminfo.ColorLevelNone - } else { - switch os.Getenv("TERM") { - // From https://github.com/jenkinsci/ansicolor-plugin/tree/e2a42bf6c6acadc46468a6bf75dbd958a4747d0b?tab=readme-ov-file#colormaps - case "xterm", "vga", "gnome-terminal", "css": - return terminfo.ColorLevelHundreds - } - } - default: - if logIsParseable { - return terminfo.ColorLevelNone - } else { - return color.DetectColorLevel() - } - } - } - - if piped, err := stdoutPiped(); err != nil || piped { - return terminfo.ColorLevelNone - } - - return color.DetectColorLevel() -} - -func stdoutPiped() (bool, error) { - fileInfo, err := os.Stdout.Stat() - if err != nil { - return false, fmt.Errorf("get stdout fileinfo: %w", err) - } - - piped := (fileInfo.Mode() & os.ModeCharDevice) == 0 - - return piped, nil -} diff --git a/pkg/log/logger_logboek.go b/pkg/log/logger_logboek.go index 0694a434..7efde137 100644 --- a/pkg/log/logger_logboek.go +++ b/pkg/log/logger_logboek.go @@ -9,7 +9,7 @@ import ( "github.com/gookit/color" "github.com/samber/lo" - "github.com/werf/kubedog/pkg/trackers/dyntracker/util" + "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/logboek" "github.com/werf/logboek/pkg/level" ) diff --git a/pkg/plan/legacy_progress_reporter.go b/pkg/plan/legacy_progress_reporter.go index 9f47e2b3..ba8e7967 100644 --- a/pkg/plan/legacy_progress_reporter.go +++ b/pkg/plan/legacy_progress_reporter.go @@ -6,7 +6,7 @@ import ( "github.com/samber/lo" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/nelm/pkg/legacy/progrep" ) diff --git a/pkg/plan/operation_config.go b/pkg/plan/operation_config.go index 84765b0e..c94b59bd 100644 --- a/pkg/plan/operation_config.go +++ b/pkg/plan/operation_config.go @@ -1,13 +1,14 @@ package plan import ( + "fmt" "regexp" "time" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/resource/spec" ) @@ -113,7 +114,7 @@ func (c *OperationConfigDelete) IDHuman() string { type OperationConfigTrackReadiness struct { ResourceMeta *spec.ResourceMeta `json:"resourceMeta"` - FailMode multitrack.FailMode `json:"failMode"` + FailMode statestore.FailMode `json:"failMode"` FailuresAllowed int `json:"failuresAllowed"` IgnoreLogs bool `json:"ignoreLogs"` IgnoreLogsForContainers []string `json:"ignoreLogsForContainers,omitempty"` @@ -165,11 +166,11 @@ type OperationConfigCreateRelease struct { } func (c *OperationConfigCreateRelease) ID() string { - return c.Release.ID() + return releaseID(c.Release.Namespace, c.Release.Name, c.Release.Version) } func (c *OperationConfigCreateRelease) IDHuman() string { - return c.Release.IDHuman() + return releaseIDHuman(c.Release.Namespace, c.Release.Name, c.Release.Version) } type OperationConfigUpdateRelease struct { @@ -177,11 +178,11 @@ type OperationConfigUpdateRelease struct { } func (c *OperationConfigUpdateRelease) ID() string { - return c.Release.ID() + return releaseID(c.Release.Namespace, c.Release.Name, c.Release.Version) } func (c *OperationConfigUpdateRelease) IDHuman() string { - return c.Release.IDHuman() + return releaseIDHuman(c.Release.Namespace, c.Release.Name, c.Release.Version) } type OperationConfigDeleteRelease struct { @@ -191,9 +192,17 @@ type OperationConfigDeleteRelease struct { } func (c *OperationConfigDeleteRelease) ID() string { - return helmrelease.ReleaseID(c.ReleaseNamespace, c.ReleaseName, c.ReleaseRevision) + return releaseID(c.ReleaseNamespace, c.ReleaseName, c.ReleaseRevision) } func (c *OperationConfigDeleteRelease) IDHuman() string { - return helmrelease.ReleaseIDHuman(c.ReleaseNamespace, c.ReleaseName, c.ReleaseRevision) + return releaseIDHuman(c.ReleaseNamespace, c.ReleaseName, c.ReleaseRevision) +} + +func releaseID(namespace, name string, revision int) string { + return fmt.Sprintf("%s:%s:%d", namespace, name, revision) +} + +func releaseIDHuman(namespace, name string, revision int) string { + return fmt.Sprintf("%s/%d (namespace=%s)", name, revision, namespace) } diff --git a/pkg/plan/plan_build.go b/pkg/plan/plan_build.go index 89e6749a..e414aab0 100644 --- a/pkg/plan/plan_build.go +++ b/pkg/plan/plan_build.go @@ -8,7 +8,8 @@ import ( "github.com/samber/lo" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasecommon "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/resource" ) @@ -99,33 +100,23 @@ func connectInternalDeployDependencies(plan *Plan, instInfos []*InstallableResou } for _, dep := range internalDeps { - var ( - dependUponOp *Operation - dependUponOpFound bool - ) + var dependUponOps []*Operation switch dep.ResourceState { case common.ResourceStatePresent: - dependUponOp, dependUponOpFound = findDeployOpInStage(plan, instInfos, dep, info.Stage) + dependUponOps = findDeployOpInStage(plan, instInfos, dep, info.Stage) case common.ResourceStateReady: - dependUponOp, dependUponOpFound = findTrackReadinessOpInStage(plan, instInfos, dep, info.Stage) + dependUponOps = findTrackReadinessOpInStage(plan, instInfos, dep, info.Stage) case common.ResourceStateAbsent: - // TODO(major): all deploy/delete dependencies must depend upon all matched operations, not a single one - dependUponOps := findTrackAbsenceOpInStage(plan, delInfos, instInfos, dep, info.Stage) - if len(dependUponOps) > 0 { - dependUponOp = dependUponOps[0] - dependUponOpFound = true - } + dependUponOps = findTrackAbsenceOpInStage(plan, delInfos, instInfos, dep, info.Stage) default: panic("unexpected internal dependency resource state") } - if !dependUponOpFound { - continue - } - - if err := plan.Connect(dependUponOp.ID(), deployOp.ID()); err != nil { - return fmt.Errorf("depend %q from %q: %w", deployOp.ID(), dependUponOp.ID(), err) + for _, dependUponOp := range dependUponOps { + if err := plan.Connect(dependUponOp.ID(), deployOp.ID()); err != nil { + return fmt.Errorf("depend %q from %q: %w", deployOp.ID(), dependUponOp.ID(), err) + } } } } @@ -146,9 +137,13 @@ func addFailureReleaseOperations(failedPlan, plan *Plan, releaseInfos []*Release switch config := op.Config.(type) { case *OperationConfigCreateRelease: - return config.Release.ID() == info.Release.ID() + return config.Release.Namespace == info.Release.Namespace && + config.Release.Name == info.Release.Name && + config.Release.Version == info.Release.Version case *OperationConfigUpdateRelease: - return config.Release.ID() == info.Release.ID() + return config.Release.Namespace == info.Release.Namespace && + config.Release.Name == info.Release.Name && + config.Release.Version == info.Release.Version default: return false } @@ -168,15 +163,15 @@ func addReleaseOperations(plan *Plan, releaseInfos []*ReleaseInfo) error { for _, info := range releaseInfos { switch info.Must { case ReleaseTypeInstall: - if err := addPendingAndDeployedReleaseOps(plan, info, helmrelease.StatusPendingInstall); err != nil { + if err := addPendingAndDeployedReleaseOps(plan, info, helmreleasecommon.StatusPendingInstall); err != nil { return fmt.Errorf("add pending/deployed ops for release install: %w", err) } case ReleaseTypeUpgrade: - if err := addPendingAndDeployedReleaseOps(plan, info, helmrelease.StatusPendingUpgrade); err != nil { + if err := addPendingAndDeployedReleaseOps(plan, info, helmreleasecommon.StatusPendingUpgrade); err != nil { return fmt.Errorf("add pending/deployed ops for release upgrade: %w", err) } case ReleaseTypeRollback: - if err := addPendingAndDeployedReleaseOps(plan, info, helmrelease.StatusPendingRollback); err != nil { + if err := addPendingAndDeployedReleaseOps(plan, info, helmreleasecommon.StatusPendingRollback); err != nil { return fmt.Errorf("add pending/deployed ops for release rollback: %w", err) } case ReleaseTypeSupersede: @@ -231,24 +226,32 @@ func connectInternalDeleteDependencies(plan *Plan, delInfos []*DeletableResource return nil } -func findDeployOpInStage(plan *Plan, instInfos []*InstallableResourceInfo, dep *resource.InternalDependency, sourceStage common.Stage) (*Operation, bool) { - var match *InstallableResourceInfo +func findDeployOpInStage(plan *Plan, instInfos []*InstallableResourceInfo, dep *resource.InternalDependency, sourceStage common.Stage) []*Operation { + matchByID := make(map[string]*InstallableResourceInfo) for _, candidate := range instInfos { + match, found := matchByID[candidate.ID()] if candidate.MustInstall == ResourceInstallTypeNone || candidate.Stage != sourceStage || !dep.Match(candidate.ResourceMeta) || - (match != nil && candidate.Iteration >= match.Iteration) { + (found && candidate.Iteration >= match.Iteration) { continue } - match = candidate + matchByID[candidate.ID()] = candidate } - if match == nil { - return nil, false + if len(matchByID) == 0 { + return nil + } + + var foundOps []*Operation + for _, match := range matchByID { + if op, found := getDeployOp(plan, match); found { + foundOps = append(foundOps, op) + } } - return getDeployOp(plan, match) + return foundOps } func addDeleteReleaseOps(plan *Plan, info *ReleaseInfo) { @@ -311,7 +314,7 @@ func addFailedReleaseOps(plan *Plan, info *ReleaseInfo) error { failedRel = rel.(*helmrelease.Release) } - failedRel.Info.Status = helmrelease.StatusFailed + failedRel.Info.Status = helmreleasecommon.StatusFailed failedOp := &Operation{ Type: OperationTypeUpdateRelease, @@ -551,7 +554,7 @@ func addMainStages(plan *Plan) error { return nil } -func addPendingAndDeployedReleaseOps(plan *Plan, info *ReleaseInfo, pendingStatus helmrelease.Status) error { +func addPendingAndDeployedReleaseOps(plan *Plan, info *ReleaseInfo, pendingStatus helmreleasecommon.Status) error { var pendingRel *helmrelease.Release if rel, err := copystructure.Copy(info.Release); err != nil { return fmt.Errorf("deep copy release: %w", err) @@ -578,7 +581,7 @@ func addPendingAndDeployedReleaseOps(plan *Plan, info *ReleaseInfo, pendingStatu succeededRel = rel.(*helmrelease.Release) } - succeededRel.Info.Status = helmrelease.StatusDeployed + succeededRel.Info.Status = helmreleasecommon.StatusDeployed succeededOp := &Operation{ Type: OperationTypeUpdateRelease, @@ -601,7 +604,7 @@ func addSupersedeReleaseOps(plan *Plan, info *ReleaseInfo) error { supersededRel = rel.(*helmrelease.Release) } - supersededRel.Info.Status = helmrelease.StatusSuperseded + supersededRel.Info.Status = helmreleasecommon.StatusSuperseded supersedeOp := &Operation{ Type: OperationTypeUpdateRelease, @@ -624,7 +627,7 @@ func addUninstallReleaseOps(plan *Plan, info *ReleaseInfo) error { uninstallingRel = rel.(*helmrelease.Release) } - uninstallingRel.Info.Status = helmrelease.StatusUninstalling + uninstallingRel.Info.Status = helmreleasecommon.StatusUninstalling uninstallingOp := &Operation{ Type: OperationTypeUpdateRelease, @@ -725,52 +728,61 @@ func findTrackAbsenceOpInStage(plan *Plan, delInfos []*DeletableResourceInfo, in return foundOps } - var match *InstallableResourceInfo + matchByID := make(map[string]*InstallableResourceInfo) for _, candidate := range instInfos { + match, found := matchByID[candidate.ID()] if !candidate.MustDeleteOnSuccessfulInstall || candidate.StageDeleteOnSuccessfulInstall != sourceStage || !dep.Match(candidate.ResourceMeta) || - (match != nil && candidate.Iteration >= match.Iteration) { + (found && candidate.Iteration >= match.Iteration) { continue } - match = candidate + matchByID[candidate.ID()] = candidate } - if match == nil { + if len(matchByID) == 0 { return nil } - opID := OperationID(OperationTypeTrackAbsence, OperationVersionTrackAbsence, OperationIteration(match.Iteration), match.ID()) + for _, match := range matchByID { + opID := OperationID(OperationTypeTrackAbsence, OperationVersionTrackAbsence, OperationIteration(match.Iteration), match.ID()) - op, found := plan.Operation(opID) - if !found { - return nil + if op, found := plan.Operation(opID); found { + foundOps = append(foundOps, op) + } } - return []*Operation{op} + return foundOps } -func findTrackReadinessOpInStage(plan *Plan, instInfos []*InstallableResourceInfo, dep *resource.InternalDependency, sourceStage common.Stage) (*Operation, bool) { - var match *InstallableResourceInfo +func findTrackReadinessOpInStage(plan *Plan, instInfos []*InstallableResourceInfo, dep *resource.InternalDependency, sourceStage common.Stage) []*Operation { + matchByID := make(map[string]*InstallableResourceInfo) for _, candidate := range instInfos { + match, found := matchByID[candidate.ID()] if !candidate.MustTrackReadiness || candidate.Stage != sourceStage || !dep.Match(candidate.ResourceMeta) || - (match != nil && candidate.Iteration >= match.Iteration) { + (found && candidate.Iteration >= match.Iteration) { continue } - match = candidate + matchByID[candidate.ID()] = candidate } - if match == nil { - return nil, false + if len(matchByID) == 0 { + return nil } - opID := OperationID(OperationTypeTrackReadiness, OperationVersionTrackReadiness, OperationIteration(match.Iteration), match.ID()) + var foundOps []*Operation + for _, match := range matchByID { + opID := OperationID(OperationTypeTrackReadiness, OperationVersionTrackReadiness, OperationIteration(match.Iteration), match.ID()) + if op, found := plan.Operation(opID); found { + foundOps = append(foundOps, op) + } + } - return plan.Operation(opID) + return foundOps } func getDeleteOp(plan *Plan, info *DeletableResourceInfo) (*Operation, bool) { diff --git a/pkg/plan/plan_build_ai_test.go b/pkg/plan/plan_build_ai_test.go new file mode 100644 index 00000000..48ee21a7 --- /dev/null +++ b/pkg/plan/plan_build_ai_test.go @@ -0,0 +1,878 @@ +//go:build ai_tests + +package plan_test + +import ( + "fmt" + "testing" + + "github.com/dominikbraun/graph" + "github.com/stretchr/testify/suite" + + "github.com/werf/nelm/pkg/common" + "github.com/werf/nelm/pkg/plan" + "github.com/werf/nelm/pkg/resource" + "github.com/werf/nelm/pkg/resource/spec" +) + +type BuildPlanAISuite struct { + BuildPlanSuite +} + +func (s *BuildPlanAISuite) TestAI_BuildPlanConnectsAllMatchingDependencies() { + testCases := []buildPlanTestCase{ + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + createOp1 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[0].LocalResource.ResourceSpec, + }, + } + + createOp2 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[1].LocalResource.ResourceSpec, + }, + } + + createDependentOp := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[2].LocalResource.ResourceSpec, + }, + } + + mainStageStartOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageStartSuffix), + }, + } + + mainStageEndOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageEndSuffix), + }, + } + + ops := []*plan.Operation{ + mainStageStartOp, + createOp1, + createOp2, + createDependentOp, + mainStageEndOp, + } + + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): { + createOp1.ID(): {}, + createOp2.ID(): {}, + }, + createOp1.ID(): { + createDependentOp.ID(): {}, + }, + createOp2.ID(): { + createDependentOp.ID(): {}, + }, + createDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + res1 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res1.Name = "test-configmap-1" + res1.Unstruct.SetName("test-configmap-1") + res1.Weight = nil + + res2 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res2.Name = "test-configmap-2" + res2.Unstruct.SetName("test-configmap-2") + res2.Weight = nil + + dependentRes := defaultInstallableResource(s.releaseName, s.releaseNamespace) + dependentRes.Name = "dependent-secret" + dependentRes.Unstruct.SetName("dependent-secret") + dependentRes.Unstruct.SetKind("Secret") + dependentRes.GroupVersionKind.Kind = "Secret" + dependentRes.Weight = nil + dependentRes.AutoInternalDependencies = []*resource.InternalDependency{ + { + ResourceMatcher: &spec.ResourceMatcher{ + Kinds: []string{"ConfigMap"}, + }, + ResourceState: common.ResourceStatePresent, + }, + } + + info1 := defaultInstallableResourceInfo(res1) + info1.MustTrackReadiness = false + + info2 := defaultInstallableResourceInfo(res2) + info2.MustTrackReadiness = false + + dependentInfo := defaultInstallableResourceInfo(dependentRes) + dependentInfo.MustTrackReadiness = false + + return []*plan.InstallableResourceInfo{info1, info2, dependentInfo}, nil, nil, plan.BuildPlanOptions{} + }, + name: "connect deploy dependency to all matching create operations", + }, + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + createOp1 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[0].LocalResource.ResourceSpec, + }, + } + + createOp2 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[1].LocalResource.ResourceSpec, + }, + } + + createDependentOp := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[2].LocalResource.ResourceSpec, + }, + } + + trackReadinessOp1 := &plan.Operation{ + Type: plan.OperationTypeTrackReadiness, + Version: plan.OperationVersionTrackReadiness, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackReadiness{ + ResourceMeta: installableInfos[0].LocalResource.ResourceMeta, + FailMode: installableInfos[0].LocalResource.FailMode, + FailuresAllowed: installableInfos[0].LocalResource.FailuresAllowed, + IgnoreLogs: installableInfos[0].LocalResource.SkipLogs, + IgnoreLogsForContainers: installableInfos[0].LocalResource.SkipLogsForContainers, + IgnoreReadinessProbeFailsByContainerName: installableInfos[0].LocalResource.IgnoreReadinessProbeFailsForContainers, + NoActivityTimeout: installableInfos[0].LocalResource.NoActivityTimeout, + SaveEvents: installableInfos[0].LocalResource.ShowServiceMessages, + SaveLogsByRegex: installableInfos[0].LocalResource.LogRegex, + SaveLogsByRegexForContainers: installableInfos[0].LocalResource.LogRegexesForContainers, + SaveLogsOnlyForContainers: installableInfos[0].LocalResource.ShowLogsOnlyForContainers, + SaveLogsOnlyForNumberOfReplicas: installableInfos[0].LocalResource.ShowLogsOnlyForNumberOfReplicas, + }, + } + + trackReadinessOp2 := &plan.Operation{ + Type: plan.OperationTypeTrackReadiness, + Version: plan.OperationVersionTrackReadiness, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackReadiness{ + ResourceMeta: installableInfos[1].LocalResource.ResourceMeta, + FailMode: installableInfos[1].LocalResource.FailMode, + FailuresAllowed: installableInfos[1].LocalResource.FailuresAllowed, + IgnoreLogs: installableInfos[1].LocalResource.SkipLogs, + IgnoreLogsForContainers: installableInfos[1].LocalResource.SkipLogsForContainers, + IgnoreReadinessProbeFailsByContainerName: installableInfos[1].LocalResource.IgnoreReadinessProbeFailsForContainers, + NoActivityTimeout: installableInfos[1].LocalResource.NoActivityTimeout, + SaveEvents: installableInfos[1].LocalResource.ShowServiceMessages, + SaveLogsByRegex: installableInfos[1].LocalResource.LogRegex, + SaveLogsByRegexForContainers: installableInfos[1].LocalResource.LogRegexesForContainers, + SaveLogsOnlyForContainers: installableInfos[1].LocalResource.ShowLogsOnlyForContainers, + SaveLogsOnlyForNumberOfReplicas: installableInfos[1].LocalResource.ShowLogsOnlyForNumberOfReplicas, + }, + } + + mainStageStartOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageStartSuffix), + }, + } + + mainStageEndOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageEndSuffix), + }, + } + + ops := []*plan.Operation{ + mainStageStartOp, + createOp1, + trackReadinessOp1, + createOp2, + trackReadinessOp2, + createDependentOp, + mainStageEndOp, + } + + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): { + createOp1.ID(): {}, + createOp2.ID(): {}, + }, + createOp1.ID(): { + trackReadinessOp1.ID(): {}, + }, + trackReadinessOp1.ID(): { + createDependentOp.ID(): {}, + }, + createOp2.ID(): { + trackReadinessOp2.ID(): {}, + }, + trackReadinessOp2.ID(): { + createDependentOp.ID(): {}, + }, + createDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + res1 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res1.Name = "test-configmap-1" + res1.Unstruct.SetName("test-configmap-1") + res1.Weight = nil + + res2 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res2.Name = "test-configmap-2" + res2.Unstruct.SetName("test-configmap-2") + res2.Weight = nil + + dependentRes := defaultInstallableResource(s.releaseName, s.releaseNamespace) + dependentRes.Name = "dependent-secret" + dependentRes.Unstruct.SetName("dependent-secret") + dependentRes.Unstruct.SetKind("Secret") + dependentRes.GroupVersionKind.Kind = "Secret" + dependentRes.Weight = nil + dependentRes.AutoInternalDependencies = []*resource.InternalDependency{ + { + ResourceMatcher: &spec.ResourceMatcher{ + Kinds: []string{"ConfigMap"}, + }, + ResourceState: common.ResourceStateReady, + }, + } + + info1 := defaultInstallableResourceInfo(res1) + info2 := defaultInstallableResourceInfo(res2) + dependentInfo := defaultInstallableResourceInfo(dependentRes) + dependentInfo.MustTrackReadiness = false + + return []*plan.InstallableResourceInfo{info1, info2, dependentInfo}, nil, nil, plan.BuildPlanOptions{} + }, + name: "connect deploy dependency to all matching track-readiness operations", + }, + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + deleteOp1 := &plan.Operation{ + Type: plan.OperationTypeDelete, + Version: plan.OperationVersionDelete, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigDelete{ + ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta, + DeletePropagation: deletableInfos[0].LocalResource.DeletePropagation, + }, + } + + trackAbsenceOp1 := &plan.Operation{ + Type: plan.OperationTypeTrackAbsence, + Version: plan.OperationVersionTrackAbsence, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackAbsence{ + ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta, + }, + } + + deleteOp2 := &plan.Operation{ + Type: plan.OperationTypeDelete, + Version: plan.OperationVersionDelete, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigDelete{ + ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta, + DeletePropagation: deletableInfos[1].LocalResource.DeletePropagation, + }, + } + + trackAbsenceOp2 := &plan.Operation{ + Type: plan.OperationTypeTrackAbsence, + Version: plan.OperationVersionTrackAbsence, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackAbsence{ + ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta, + }, + } + + createDependentOp := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ + ResourceSpec: installableInfos[0].LocalResource.ResourceSpec, + }, + } + + mainStageStartOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageStartSuffix), + }, + } + + mainStageEndOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageEndSuffix), + }, + } + + ops := []*plan.Operation{ + mainStageStartOp, + deleteOp1, + trackAbsenceOp1, + deleteOp2, + trackAbsenceOp2, + createDependentOp, + mainStageEndOp, + } + + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): { + deleteOp1.ID(): {}, + deleteOp2.ID(): {}, + }, + deleteOp1.ID(): { + trackAbsenceOp1.ID(): {}, + }, + trackAbsenceOp1.ID(): { + createDependentOp.ID(): {}, + }, + deleteOp2.ID(): { + trackAbsenceOp2.ID(): {}, + }, + trackAbsenceOp2.ID(): { + createDependentOp.ID(): {}, + }, + createDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + delRes1 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes1.Name = "test-configmap-1" + + delRes2 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes2.Name = "test-configmap-2" + + delInfo1 := defaultDeletableResourceInfo(delRes1, s.releaseName, s.releaseNamespace) + delInfo1.Stage = common.StageInstall + + delInfo2 := defaultDeletableResourceInfo(delRes2, s.releaseName, s.releaseNamespace) + delInfo2.Stage = common.StageInstall + + dependentRes := defaultInstallableResource(s.releaseName, s.releaseNamespace) + dependentRes.Name = "dependent-secret" + dependentRes.Unstruct.SetName("dependent-secret") + dependentRes.Unstruct.SetKind("Secret") + dependentRes.GroupVersionKind.Kind = "Secret" + dependentRes.Weight = nil + dependentRes.AutoInternalDependencies = []*resource.InternalDependency{ + { + ResourceMatcher: &spec.ResourceMatcher{ + Kinds: []string{"ConfigMap"}, + }, + ResourceState: common.ResourceStateAbsent, + }, + } + + dependentInfo := defaultInstallableResourceInfo(dependentRes) + dependentInfo.MustTrackReadiness = false + + return []*plan.InstallableResourceInfo{dependentInfo}, []*plan.DeletableResourceInfo{delInfo1, delInfo2}, nil, plan.BuildPlanOptions{} + }, + name: "connect deploy dependency to all matching track-absence operations", + }, + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + deleteOp1 := &plan.Operation{ + Type: plan.OperationTypeDelete, + Version: plan.OperationVersionDelete, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigDelete{ + ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta, + DeletePropagation: deletableInfos[0].LocalResource.DeletePropagation, + }, + } + + trackAbsenceOp1 := &plan.Operation{ + Type: plan.OperationTypeTrackAbsence, + Version: plan.OperationVersionTrackAbsence, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackAbsence{ + ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta, + }, + } + + deleteOp2 := &plan.Operation{ + Type: plan.OperationTypeDelete, + Version: plan.OperationVersionDelete, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigDelete{ + ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta, + DeletePropagation: deletableInfos[1].LocalResource.DeletePropagation, + }, + } + + trackAbsenceOp2 := &plan.Operation{ + Type: plan.OperationTypeTrackAbsence, + Version: plan.OperationVersionTrackAbsence, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackAbsence{ + ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta, + }, + } + + deleteDependentOp := &plan.Operation{ + Type: plan.OperationTypeDelete, + Version: plan.OperationVersionDelete, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigDelete{ + ResourceMeta: deletableInfos[2].LocalResource.ResourceMeta, + DeletePropagation: deletableInfos[2].LocalResource.DeletePropagation, + }, + } + + trackAbsenceDependentOp := &plan.Operation{ + Type: plan.OperationTypeTrackAbsence, + Version: plan.OperationVersionTrackAbsence, + Category: plan.OperationCategoryTrack, + Config: &plan.OperationConfigTrackAbsence{ + ResourceMeta: deletableInfos[2].LocalResource.ResourceMeta, + }, + } + + mainStageStartOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageUninstall, common.StageStartSuffix), + }, + } + + mainStageEndOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{ + OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageUninstall, common.StageEndSuffix), + }, + } + + ops := []*plan.Operation{ + mainStageStartOp, + deleteOp1, + trackAbsenceOp1, + deleteOp2, + trackAbsenceOp2, + deleteDependentOp, + trackAbsenceDependentOp, + mainStageEndOp, + } + + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): { + deleteOp1.ID(): {}, + deleteOp2.ID(): {}, + }, + deleteOp1.ID(): { + trackAbsenceOp1.ID(): {}, + }, + trackAbsenceOp1.ID(): { + deleteDependentOp.ID(): {}, + }, + deleteOp2.ID(): { + trackAbsenceOp2.ID(): {}, + }, + trackAbsenceOp2.ID(): { + deleteDependentOp.ID(): {}, + }, + deleteDependentOp.ID(): { + trackAbsenceDependentOp.ID(): {}, + }, + trackAbsenceDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + delRes1 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes1.Name = "test-configmap-1" + + delRes2 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes2.Name = "test-configmap-2" + + dependentDelRes := defaultDeletableResource(s.releaseName, s.releaseNamespace) + dependentDelRes.Name = "dependent-secret" + dependentDelRes.GroupVersionKind.Kind = "Secret" + dependentDelRes.AutoInternalDependencies = []*resource.InternalDependency{ + { + ResourceMatcher: &spec.ResourceMatcher{ + Kinds: []string{"ConfigMap"}, + }, + ResourceState: common.ResourceStateAbsent, + }, + } + + delInfo1 := defaultDeletableResourceInfo(delRes1, s.releaseName, s.releaseNamespace) + delInfo2 := defaultDeletableResourceInfo(delRes2, s.releaseName, s.releaseNamespace) + dependentDelInfo := defaultDeletableResourceInfo(dependentDelRes, s.releaseName, s.releaseNamespace) + + return nil, []*plan.DeletableResourceInfo{delInfo1, delInfo2, dependentDelInfo}, nil, plan.BuildPlanOptions{} + }, + name: "connect delete dependency to all matching track-absence operations", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, runBuildPlanTest(tc, &s.BuildPlanSuite)) + } +} + +func (s *BuildPlanAISuite) TestAI_BuildPlanConnectsOnlySpecificMatchingDependencies() { + testCases := []buildPlanTestCase{ + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + createOp1 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[0].LocalResource.ResourceSpec}, + } + createOp2 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[1].LocalResource.ResourceSpec}, + } + createOp3 := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[2].LocalResource.ResourceSpec}, + } + createDependentOp := &plan.Operation{ + Type: plan.OperationTypeCreate, + Version: plan.OperationVersionCreate, + Category: plan.OperationCategoryResource, + Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[3].LocalResource.ResourceSpec}, + } + mainStageStartOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageStartSuffix)}, + } + mainStageEndOp := &plan.Operation{ + Type: plan.OperationTypeNoop, + Version: plan.OperationVersionNoop, + Category: plan.OperationCategoryMeta, + Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageEndSuffix)}, + } + + ops := []*plan.Operation{mainStageStartOp, createOp1, createOp2, createOp3, createDependentOp, mainStageEndOp} + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): {createOp1.ID(): {}, createOp2.ID(): {}, createOp3.ID(): {}}, + createOp1.ID(): {createDependentOp.ID(): {}}, + createOp2.ID(): {createDependentOp.ID(): {}}, + createOp3.ID(): {mainStageEndOp.ID(): {}}, + createDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + res1 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res1.Name = "test-configmap-1" + res1.Unstruct.SetName("test-configmap-1") + res1.Weight = nil + + res2 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res2.Name = "test-configmap-2" + res2.Unstruct.SetName("test-configmap-2") + res2.Weight = nil + + res3 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res3.Name = "test-configmap-3" + res3.Unstruct.SetName("test-configmap-3") + res3.Weight = nil + + dependentRes := defaultInstallableResource(s.releaseName, s.releaseNamespace) + dependentRes.Name = "dependent-secret" + dependentRes.Unstruct.SetName("dependent-secret") + dependentRes.Unstruct.SetKind("Secret") + dependentRes.GroupVersionKind.Kind = "Secret" + dependentRes.Weight = nil + dependentRes.AutoInternalDependencies = []*resource.InternalDependency{{ + ResourceMatcher: &spec.ResourceMatcher{Names: []string{"test-configmap-1", "test-configmap-2"}, Kinds: []string{"ConfigMap"}}, + ResourceState: common.ResourceStatePresent, + }} + + info1 := defaultInstallableResourceInfo(res1) + info1.MustTrackReadiness = false + info2 := defaultInstallableResourceInfo(res2) + info2.MustTrackReadiness = false + info3 := defaultInstallableResourceInfo(res3) + info3.MustTrackReadiness = false + dependentInfo := defaultInstallableResourceInfo(dependentRes) + dependentInfo.MustTrackReadiness = false + + return []*plan.InstallableResourceInfo{info1, info2, info3, dependentInfo}, nil, nil, plan.BuildPlanOptions{} + }, + name: "connect deploy dependency only to specifically matched create operations", + }, + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + createOp1 := &plan.Operation{Type: plan.OperationTypeCreate, Version: plan.OperationVersionCreate, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[0].LocalResource.ResourceSpec}} + trackReadinessOp1 := &plan.Operation{Type: plan.OperationTypeTrackReadiness, Version: plan.OperationVersionTrackReadiness, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackReadiness{ResourceMeta: installableInfos[0].LocalResource.ResourceMeta, FailMode: installableInfos[0].LocalResource.FailMode, FailuresAllowed: installableInfos[0].LocalResource.FailuresAllowed, IgnoreLogs: installableInfos[0].LocalResource.SkipLogs, IgnoreLogsForContainers: installableInfos[0].LocalResource.SkipLogsForContainers, IgnoreReadinessProbeFailsByContainerName: installableInfos[0].LocalResource.IgnoreReadinessProbeFailsForContainers, NoActivityTimeout: installableInfos[0].LocalResource.NoActivityTimeout, SaveEvents: installableInfos[0].LocalResource.ShowServiceMessages, SaveLogsByRegex: installableInfos[0].LocalResource.LogRegex, SaveLogsByRegexForContainers: installableInfos[0].LocalResource.LogRegexesForContainers, SaveLogsOnlyForContainers: installableInfos[0].LocalResource.ShowLogsOnlyForContainers, SaveLogsOnlyForNumberOfReplicas: installableInfos[0].LocalResource.ShowLogsOnlyForNumberOfReplicas}} + + createOp2 := &plan.Operation{Type: plan.OperationTypeCreate, Version: plan.OperationVersionCreate, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[1].LocalResource.ResourceSpec}} + trackReadinessOp2 := &plan.Operation{Type: plan.OperationTypeTrackReadiness, Version: plan.OperationVersionTrackReadiness, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackReadiness{ResourceMeta: installableInfos[1].LocalResource.ResourceMeta, FailMode: installableInfos[1].LocalResource.FailMode, FailuresAllowed: installableInfos[1].LocalResource.FailuresAllowed, IgnoreLogs: installableInfos[1].LocalResource.SkipLogs, IgnoreLogsForContainers: installableInfos[1].LocalResource.SkipLogsForContainers, IgnoreReadinessProbeFailsByContainerName: installableInfos[1].LocalResource.IgnoreReadinessProbeFailsForContainers, NoActivityTimeout: installableInfos[1].LocalResource.NoActivityTimeout, SaveEvents: installableInfos[1].LocalResource.ShowServiceMessages, SaveLogsByRegex: installableInfos[1].LocalResource.LogRegex, SaveLogsByRegexForContainers: installableInfos[1].LocalResource.LogRegexesForContainers, SaveLogsOnlyForContainers: installableInfos[1].LocalResource.ShowLogsOnlyForContainers, SaveLogsOnlyForNumberOfReplicas: installableInfos[1].LocalResource.ShowLogsOnlyForNumberOfReplicas}} + + createOp3 := &plan.Operation{Type: plan.OperationTypeCreate, Version: plan.OperationVersionCreate, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[2].LocalResource.ResourceSpec}} + trackReadinessOp3 := &plan.Operation{Type: plan.OperationTypeTrackReadiness, Version: plan.OperationVersionTrackReadiness, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackReadiness{ResourceMeta: installableInfos[2].LocalResource.ResourceMeta, FailMode: installableInfos[2].LocalResource.FailMode, FailuresAllowed: installableInfos[2].LocalResource.FailuresAllowed, IgnoreLogs: installableInfos[2].LocalResource.SkipLogs, IgnoreLogsForContainers: installableInfos[2].LocalResource.SkipLogsForContainers, IgnoreReadinessProbeFailsByContainerName: installableInfos[2].LocalResource.IgnoreReadinessProbeFailsForContainers, NoActivityTimeout: installableInfos[2].LocalResource.NoActivityTimeout, SaveEvents: installableInfos[2].LocalResource.ShowServiceMessages, SaveLogsByRegex: installableInfos[2].LocalResource.LogRegex, SaveLogsByRegexForContainers: installableInfos[2].LocalResource.LogRegexesForContainers, SaveLogsOnlyForContainers: installableInfos[2].LocalResource.ShowLogsOnlyForContainers, SaveLogsOnlyForNumberOfReplicas: installableInfos[2].LocalResource.ShowLogsOnlyForNumberOfReplicas}} + + createDependentOp := &plan.Operation{Type: plan.OperationTypeCreate, Version: plan.OperationVersionCreate, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[3].LocalResource.ResourceSpec}} + + mainStageStartOp := &plan.Operation{Type: plan.OperationTypeNoop, Version: plan.OperationVersionNoop, Category: plan.OperationCategoryMeta, Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageStartSuffix)}} + mainStageEndOp := &plan.Operation{Type: plan.OperationTypeNoop, Version: plan.OperationVersionNoop, Category: plan.OperationCategoryMeta, Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageEndSuffix)}} + + ops := []*plan.Operation{mainStageStartOp, createOp1, trackReadinessOp1, createOp2, trackReadinessOp2, createOp3, trackReadinessOp3, createDependentOp, mainStageEndOp} + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): {createOp1.ID(): {}, createOp2.ID(): {}, createOp3.ID(): {}}, + createOp1.ID(): {trackReadinessOp1.ID(): {}}, + trackReadinessOp1.ID(): { + createDependentOp.ID(): {}, + }, + createOp2.ID(): {trackReadinessOp2.ID(): {}}, + trackReadinessOp2.ID(): { + createDependentOp.ID(): {}, + }, + createOp3.ID(): {trackReadinessOp3.ID(): {}}, + trackReadinessOp3.ID(): { + mainStageEndOp.ID(): {}, + }, + createDependentOp.ID(): {mainStageEndOp.ID(): {}}, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + res1 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res1.Name = "test-configmap-1" + res1.Unstruct.SetName("test-configmap-1") + res1.Weight = nil + res2 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res2.Name = "test-configmap-2" + res2.Unstruct.SetName("test-configmap-2") + res2.Weight = nil + res3 := defaultInstallableResource(s.releaseName, s.releaseNamespace) + res3.Name = "test-configmap-3" + res3.Unstruct.SetName("test-configmap-3") + res3.Weight = nil + + dependentRes := defaultInstallableResource(s.releaseName, s.releaseNamespace) + dependentRes.Name = "dependent-secret" + dependentRes.Unstruct.SetName("dependent-secret") + dependentRes.Unstruct.SetKind("Secret") + dependentRes.GroupVersionKind.Kind = "Secret" + dependentRes.Weight = nil + dependentRes.AutoInternalDependencies = []*resource.InternalDependency{{ + ResourceMatcher: &spec.ResourceMatcher{Names: []string{"test-configmap-1", "test-configmap-2"}, Kinds: []string{"ConfigMap"}}, + ResourceState: common.ResourceStateReady, + }} + + info1 := defaultInstallableResourceInfo(res1) + info2 := defaultInstallableResourceInfo(res2) + info3 := defaultInstallableResourceInfo(res3) + dependentInfo := defaultInstallableResourceInfo(dependentRes) + dependentInfo.MustTrackReadiness = false + + return []*plan.InstallableResourceInfo{info1, info2, info3, dependentInfo}, nil, nil, plan.BuildPlanOptions{} + }, + name: "connect deploy dependency only to specifically matched track-readiness operations", + }, + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + deleteOp1 := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[0].LocalResource.DeletePropagation}} + trackAbsenceOp1 := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta}} + deleteOp2 := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[1].LocalResource.DeletePropagation}} + trackAbsenceOp2 := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta}} + deleteOp3 := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[2].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[2].LocalResource.DeletePropagation}} + trackAbsenceOp3 := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[2].LocalResource.ResourceMeta}} + createDependentOp := &plan.Operation{Type: plan.OperationTypeCreate, Version: plan.OperationVersionCreate, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigCreate{ResourceSpec: installableInfos[0].LocalResource.ResourceSpec}} + + mainStageStartOp := &plan.Operation{Type: plan.OperationTypeNoop, Version: plan.OperationVersionNoop, Category: plan.OperationCategoryMeta, Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageStartSuffix)}} + mainStageEndOp := &plan.Operation{Type: plan.OperationTypeNoop, Version: plan.OperationVersionNoop, Category: plan.OperationCategoryMeta, Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageInstall, common.StageEndSuffix)}} + + ops := []*plan.Operation{mainStageStartOp, deleteOp1, trackAbsenceOp1, deleteOp2, trackAbsenceOp2, deleteOp3, trackAbsenceOp3, createDependentOp, mainStageEndOp} + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): {deleteOp1.ID(): {}, deleteOp2.ID(): {}, deleteOp3.ID(): {}}, + deleteOp1.ID(): {trackAbsenceOp1.ID(): {}}, + trackAbsenceOp1.ID(): {createDependentOp.ID(): {}}, + deleteOp2.ID(): {trackAbsenceOp2.ID(): {}}, + trackAbsenceOp2.ID(): {createDependentOp.ID(): {}}, + deleteOp3.ID(): {trackAbsenceOp3.ID(): {}}, + trackAbsenceOp3.ID(): {mainStageEndOp.ID(): {}}, + createDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + delRes1 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes1.Name = "test-configmap-1" + delInfo1 := defaultDeletableResourceInfo(delRes1, s.releaseName, s.releaseNamespace) + delInfo1.Stage = common.StageInstall + + delRes2 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes2.Name = "test-configmap-2" + delInfo2 := defaultDeletableResourceInfo(delRes2, s.releaseName, s.releaseNamespace) + delInfo2.Stage = common.StageInstall + + delRes3 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes3.Name = "test-configmap-3" + delInfo3 := defaultDeletableResourceInfo(delRes3, s.releaseName, s.releaseNamespace) + delInfo3.Stage = common.StageInstall + + dependentRes := defaultInstallableResource(s.releaseName, s.releaseNamespace) + dependentRes.Name = "dependent-secret" + dependentRes.Unstruct.SetName("dependent-secret") + dependentRes.Unstruct.SetKind("Secret") + dependentRes.GroupVersionKind.Kind = "Secret" + dependentRes.Weight = nil + dependentRes.AutoInternalDependencies = []*resource.InternalDependency{{ + ResourceMatcher: &spec.ResourceMatcher{Names: []string{"test-configmap-1", "test-configmap-2"}, Kinds: []string{"ConfigMap"}}, + ResourceState: common.ResourceStateAbsent, + }} + dependentInfo := defaultInstallableResourceInfo(dependentRes) + dependentInfo.MustTrackReadiness = false + + return []*plan.InstallableResourceInfo{dependentInfo}, []*plan.DeletableResourceInfo{delInfo1, delInfo2, delInfo3}, nil, plan.BuildPlanOptions{} + }, + name: "connect deploy dependency only to specifically matched track-absence operations", + }, + { + expect: func(installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo) ([]*plan.Operation, map[string]map[string]graph.Edge[string]) { + deleteOp1 := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[0].LocalResource.DeletePropagation}} + trackAbsenceOp1 := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[0].LocalResource.ResourceMeta}} + deleteOp2 := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[1].LocalResource.DeletePropagation}} + trackAbsenceOp2 := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[1].LocalResource.ResourceMeta}} + deleteOp3 := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[2].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[2].LocalResource.DeletePropagation}} + trackAbsenceOp3 := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[2].LocalResource.ResourceMeta}} + deleteDependentOp := &plan.Operation{Type: plan.OperationTypeDelete, Version: plan.OperationVersionDelete, Category: plan.OperationCategoryResource, Config: &plan.OperationConfigDelete{ResourceMeta: deletableInfos[3].LocalResource.ResourceMeta, DeletePropagation: deletableInfos[3].LocalResource.DeletePropagation}} + trackAbsenceDependentOp := &plan.Operation{Type: plan.OperationTypeTrackAbsence, Version: plan.OperationVersionTrackAbsence, Category: plan.OperationCategoryTrack, Config: &plan.OperationConfigTrackAbsence{ResourceMeta: deletableInfos[3].LocalResource.ResourceMeta}} + + mainStageStartOp := &plan.Operation{Type: plan.OperationTypeNoop, Version: plan.OperationVersionNoop, Category: plan.OperationCategoryMeta, Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageUninstall, common.StageStartSuffix)}} + mainStageEndOp := &plan.Operation{Type: plan.OperationTypeNoop, Version: plan.OperationVersionNoop, Category: plan.OperationCategoryMeta, Config: &plan.OperationConfigNoop{OpID: fmt.Sprintf("%s/%s/%s", common.StagePrefix, common.StageUninstall, common.StageEndSuffix)}} + + ops := []*plan.Operation{mainStageStartOp, deleteOp1, trackAbsenceOp1, deleteOp2, trackAbsenceOp2, deleteOp3, trackAbsenceOp3, deleteDependentOp, trackAbsenceDependentOp, mainStageEndOp} + adjMap := map[string]map[string]graph.Edge[string]{ + mainStageStartOp.ID(): {deleteOp1.ID(): {}, deleteOp2.ID(): {}, deleteOp3.ID(): {}}, + deleteOp1.ID(): {trackAbsenceOp1.ID(): {}}, + trackAbsenceOp1.ID(): {deleteDependentOp.ID(): {}}, + deleteOp2.ID(): {trackAbsenceOp2.ID(): {}}, + trackAbsenceOp2.ID(): {deleteDependentOp.ID(): {}}, + deleteOp3.ID(): {trackAbsenceOp3.ID(): {}}, + trackAbsenceOp3.ID(): {mainStageEndOp.ID(): {}}, + deleteDependentOp.ID(): { + trackAbsenceDependentOp.ID(): {}, + }, + trackAbsenceDependentOp.ID(): { + mainStageEndOp.ID(): {}, + }, + mainStageEndOp.ID(): {}, + } + + return ops, adjMap + }, + input: func() (installableInfos []*plan.InstallableResourceInfo, deletableInfos []*plan.DeletableResourceInfo, releaseInfos []*plan.ReleaseInfo, opts plan.BuildPlanOptions) { + delRes1 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes1.Name = "test-configmap-1" + delRes2 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes2.Name = "test-configmap-2" + delRes3 := defaultDeletableResource(s.releaseName, s.releaseNamespace) + delRes3.Name = "test-configmap-3" + dependentDelRes := defaultDeletableResource(s.releaseName, s.releaseNamespace) + dependentDelRes.Name = "dependent-secret" + dependentDelRes.GroupVersionKind.Kind = "Secret" + dependentDelRes.AutoInternalDependencies = []*resource.InternalDependency{{ + ResourceMatcher: &spec.ResourceMatcher{Names: []string{"test-configmap-1", "test-configmap-2"}, Kinds: []string{"ConfigMap"}}, + ResourceState: common.ResourceStateAbsent, + }} + + delInfo1 := defaultDeletableResourceInfo(delRes1, s.releaseName, s.releaseNamespace) + delInfo2 := defaultDeletableResourceInfo(delRes2, s.releaseName, s.releaseNamespace) + delInfo3 := defaultDeletableResourceInfo(delRes3, s.releaseName, s.releaseNamespace) + dependentDelInfo := defaultDeletableResourceInfo(dependentDelRes, s.releaseName, s.releaseNamespace) + + return nil, []*plan.DeletableResourceInfo{delInfo1, delInfo2, delInfo3, dependentDelInfo}, nil, plan.BuildPlanOptions{} + }, + name: "connect delete dependency only to specifically matched track-absence operations", + }, + } + + for _, tc := range testCases { + s.Run(tc.name, runBuildPlanTest(tc, &s.BuildPlanSuite)) + } +} + +func TestAI_BuildPlanSuiteMultiMatchDependencies(t *testing.T) { + suite.Run(t, new(BuildPlanAISuite)) +} diff --git a/pkg/plan/plan_build_test.go b/pkg/plan/plan_build_test.go index 6584a55d..fc8fe24d 100644 --- a/pkg/plan/plan_build_test.go +++ b/pkg/plan/plan_build_test.go @@ -12,7 +12,8 @@ import ( "github.com/stretchr/testify/suite" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasestatus "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/plan" "github.com/werf/nelm/pkg/resource" "github.com/werf/nelm/pkg/resource/spec" @@ -229,7 +230,7 @@ func (s *BuildPlanSuite) TestBuildPlan() { s.Require().NoError(err) updatedRel := updatedRelRaw.(*helmrelease.Release) - updatedRel.Info.Status = helmrelease.StatusDeployed + updatedRel.Info.Status = helmreleasestatus.StatusDeployed updateReleaseOp := &plan.Operation{ Type: plan.OperationTypeUpdateRelease, @@ -1415,7 +1416,7 @@ func defaultRelease(releaseName, releaseNamespace string) *helmrelease.Release { Name: releaseName, Namespace: releaseNamespace, Info: &helmrelease.Info{ - Status: helmrelease.StatusPendingInstall, + Status: helmreleasestatus.StatusPendingInstall, }, Version: 1, } diff --git a/pkg/plan/plan_execute.go b/pkg/plan/plan_execute.go index c70fa2e1..da97d655 100644 --- a/pkg/plan/plan_execute.go +++ b/pkg/plan/plan_execute.go @@ -9,11 +9,11 @@ import ( "github.com/samber/lo" "github.com/sourcegraph/conc/pool" + "github.com/werf/kubedog/pkg/dyntracker" + "github.com/werf/kubedog/pkg/dyntracker/logstore" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/kubedog/pkg/informer" - "github.com/werf/kubedog/pkg/trackers/dyntracker" - "github.com/werf/kubedog/pkg/trackers/dyntracker/logstore" - "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" diff --git a/pkg/plan/planned_changes.go b/pkg/plan/planned_changes.go index bc9101e6..65172868 100644 --- a/pkg/plan/planned_changes.go +++ b/pkg/plan/planned_changes.go @@ -18,9 +18,7 @@ import ( const ( HiddenInsignificantChanges = "" - HiddenSensitiveChanges = "" HiddenVerboseCRDChanges = "" - HiddenVerboseChanges = "" ) type ResourceChange struct { @@ -44,10 +42,6 @@ func (c *ResourceChange) UDiff(opts common.ResourceDiffOptions) (string, error) !opts.ShowVerboseCRDDiffs && (c.Before == nil || c.After == nil) { uDiff = HiddenVerboseCRDChanges - } else if sensitiveInfo.FullySensitive() && !opts.ShowSensitiveDiffs { - uDiff = HiddenSensitiveChanges - } else if !opts.ShowVerboseDiffs && (c.Before == nil || c.After == nil) { - uDiff = HiddenVerboseChanges } else { var ( oldObjManifest string diff --git a/pkg/plan/release_info.go b/pkg/plan/release_info.go index 01a4ed96..27f35055 100644 --- a/pkg/plan/release_info.go +++ b/pkg/plan/release_info.go @@ -4,7 +4,8 @@ import ( "context" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmreleasecommon "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" ) const ( @@ -50,7 +51,7 @@ func BuildReleaseInfos(ctx context.Context, deployType common.DeployType, prevRe }) for _, rel := range prevReleases { - if rel.Info.Status == helmrelease.StatusDeployed { + if rel.Info.Status == helmreleasecommon.StatusDeployed { infos = append(infos, &ReleaseInfo{ Must: ReleaseTypeSupersede, Release: rel, @@ -65,7 +66,7 @@ func BuildReleaseInfos(ctx context.Context, deployType common.DeployType, prevRe }) for _, rel := range prevReleases { - if rel.Info.Status == helmrelease.StatusDeployed { + if rel.Info.Status == helmreleasecommon.StatusDeployed { infos = append(infos, &ReleaseInfo{ Must: ReleaseTypeSupersede, Release: rel, @@ -80,7 +81,7 @@ func BuildReleaseInfos(ctx context.Context, deployType common.DeployType, prevRe }) for _, rel := range prevReleases { - if rel.Info.Status == helmrelease.StatusDeployed { + if rel.Info.Status == helmreleasecommon.StatusDeployed { infos = append(infos, &ReleaseInfo{ Must: ReleaseTypeSupersede, Release: rel, diff --git a/pkg/plan/resource_info.go b/pkg/plan/resource_info.go index 3a3187de..ed7590eb 100644 --- a/pkg/plan/resource_info.go +++ b/pkg/plan/resource_info.go @@ -2,6 +2,7 @@ package plan import ( "context" + "encoding/json" "fmt" "sort" "strings" @@ -13,9 +14,8 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/json" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" + "github.com/werf/kubedog/pkg/dyntracker/statestore" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/log" @@ -216,6 +216,12 @@ func buildInstallableResourceInfo(ctx context.Context, localRes *resource.Instal return nil, fmt.Errorf("determine install type for resource %q: %w", localRes.IDHuman(), err) } + if installType == ResourceInstallTypeRecreate { + if _, found := localRes.Annotations[common.AnnotationKeyHumanResourcePolicy]; found { + return nil, fmt.Errorf("cannot recreate the resource %q because its deletion is prohibited", localRes.IDHuman()) + } + } + getMeta := spec.NewResourceMetaFromUnstructured(getObj, releaseNamespace, localRes.FilePath) mustDeleteOnSuccess := mustDeleteOnSuccessfulDeploy(localRes, getMeta, installType, releaseNamespace) trackReadiness := mustTrackReadiness(localRes, installType, true, prevRelFailed, mustDeleteOnSuccess) @@ -726,7 +732,7 @@ func mustDeleteOnSuccessfulDeploy(localRes *resource.InstallableResource, getMet func mustTrackReadiness(res *resource.InstallableResource, resInstallType ResourceInstallType, exists, prevRelFailed, mustDeleteOnSuccessfulInstall bool) bool { if spec.IsCRD(res.Unstruct.GroupVersionKind().GroupKind()) || - res.TrackTerminationMode == multitrack.NonBlocking { + res.TrackTerminationMode == statestore.NonBlocking { return false } diff --git a/pkg/plan/resource_info_test.go b/pkg/plan/resource_info_test.go index fad8a1cd..3bad6fc5 100644 --- a/pkg/plan/resource_info_test.go +++ b/pkg/plan/resource_info_test.go @@ -11,7 +11,7 @@ import ( "github.com/stretchr/testify/suite" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" + "github.com/werf/kubedog/pkg/dyntracker/statestore" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/kube/fake" @@ -539,10 +539,10 @@ func defaultInstallableResource(releaseName, releaseNamespace string) *resource. return &resource.InstallableResource{ ResourceSpec: defaultResourceSpec(releaseName, releaseNamespace), Ownership: common.OwnershipRelease, - FailMode: multitrack.FailWholeDeployProcessImmediately, + FailMode: statestore.FailWholeDeployProcessImmediately, NoActivityTimeout: 4 * time.Minute, ShowLogsOnlyForNumberOfReplicas: 1, - TrackTerminationMode: multitrack.WaitUntilResourceReady, + TrackTerminationMode: statestore.WaitUntilResourceReady, Weight: lo.ToPtr(0), DeployConditions: map[common.On][]common.Stage{ common.InstallOnInstall: {common.StageInstall}, diff --git a/pkg/release/history.go b/pkg/release/history.go index 39e587f0..67a7517b 100644 --- a/pkg/release/history.go +++ b/pkg/release/history.go @@ -2,16 +2,18 @@ package release import ( "context" + "errors" "fmt" "slices" "sync" + "time" "github.com/samber/lo" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" + helmreleasecommon "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" + releaseutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" "github.com/werf/nelm/pkg/helm/pkg/storage/driver" - helmtime "github.com/werf/nelm/pkg/helm/pkg/time" ) var _ Historier = (*History)(nil) @@ -47,7 +49,7 @@ func (h *History) CreateRelease(ctx context.Context, rel *helmrelease.Release) e h.updateLock.Lock() defer h.updateLock.Unlock() - rel.Info.FirstDeployed = helmtime.Now() + rel.Info.FirstDeployed = time.Now() rel.Info.LastDeployed = rel.Info.FirstDeployed if err := h.storage.Create(rel); err != nil { @@ -81,8 +83,8 @@ func (h *History) DeleteRelease(ctx context.Context, name string, revision int) func (h *History) FindAllDeployed() []*helmrelease.Release { _, lastUninstalledRelIndex, lastUninstalledRelFound := lo.FindLastIndexOf(h.releases, func(r *helmrelease.Release) bool { - return r.Info.Status == helmrelease.StatusUninstalled || - r.Info.Status == helmrelease.StatusUninstalling + return r.Info.Status == helmreleasecommon.StatusUninstalled || + r.Info.Status == helmreleasecommon.StatusUninstalling }) var relsSinceUninstalled []*helmrelease.Release @@ -97,8 +99,8 @@ func (h *History) FindAllDeployed() []*helmrelease.Release { } return lo.Filter(relsSinceUninstalled, func(r *helmrelease.Release, _ int) bool { - return r.Info.Status == helmrelease.StatusDeployed || - r.Info.Status == helmrelease.StatusSuperseded + return r.Info.Status == helmreleasecommon.StatusDeployed || + r.Info.Status == helmreleasecommon.StatusSuperseded }) } @@ -116,7 +118,7 @@ func (h *History) UpdateRelease(ctx context.Context, rel *helmrelease.Release) e h.updateLock.Lock() defer h.updateLock.Unlock() - rel.Info.FirstDeployed = helmtime.Now() + rel.Info.FirstDeployed = time.Now() rel.Info.LastDeployed = rel.Info.FirstDeployed if err := h.storage.Update(rel); err != nil { @@ -139,7 +141,7 @@ type HistoryOptions struct{} // Builds histories for multiple different releases. func BuildHistories(historyStorage ReleaseStorager, opts HistoryOptions) ([]*History, error) { rels, err := historyStorage.Query(map[string]string{"owner": "helm"}) - if err != nil && err != driver.ErrReleaseNotFound { + if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { return nil, fmt.Errorf("query releases: %w", err) } @@ -176,7 +178,7 @@ func BuildHistories(historyStorage ReleaseStorager, opts HistoryOptions) ([]*His // Builds history for a specific release. func BuildHistory(releaseName string, historyStorage ReleaseStorager, opts HistoryOptions) (*History, error) { rels, err := historyStorage.Query(map[string]string{"name": releaseName, "owner": "helm"}) - if err != nil && err != driver.ErrReleaseNotFound { + if err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { return nil, fmt.Errorf("query releases for release %q: %w", releaseName, err) } diff --git a/pkg/release/release.go b/pkg/release/release.go index 17eb27cb..2bf6b568 100644 --- a/pkg/release/release.go +++ b/pkg/release/release.go @@ -15,11 +15,13 @@ import ( "sigs.k8s.io/yaml" "github.com/werf/nelm/pkg/common" - helmchart "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" - "github.com/werf/nelm/pkg/helm/pkg/releaseutil" + helmchart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" + chartv2util "github.com/werf/nelm/pkg/helm/pkg/chart/v2/util" + helmreleasecommon "github.com/werf/nelm/pkg/helm/pkg/release/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" + releaseutil "github.com/werf/nelm/pkg/helm/pkg/release/v1/util" "github.com/werf/nelm/pkg/resource/spec" + "github.com/werf/nelm/pkg/util" ) type ReleaseOptions struct { @@ -39,7 +41,7 @@ func IsReleaseUpToDate(oldRel, newRel *helmrelease.Release) (bool, error) { cmpopts.EquateEmpty(), } - if oldRel.Info.Status != helmrelease.StatusDeployed || + if oldRel.Info.Status != helmreleasecommon.StatusDeployed || oldRel.Info.Notes != newRel.Info.Notes || !cmp.Equal(oldRel.Config, newRel.Config, cmpOpts) { return false, nil @@ -77,7 +79,7 @@ func IsReleaseUpToDate(oldRel, newRel *helmrelease.Release) (bool, error) { return false, nil } - oldRelManifests := releaseutil.SplitManifestsToSlice(oldRel.Manifest) + oldRelManifests := util.SplitManifests(oldRel.Manifest) oldRegularResourcesHash := fnv.New32a() for _, manifest := range oldRelManifests { @@ -93,7 +95,7 @@ func IsReleaseUpToDate(oldRel, newRel *helmrelease.Release) (bool, error) { } } - newRelManifests := releaseutil.SplitManifestsToSlice(newRel.Manifest) + newRelManifests := util.SplitManifests(newRel.Manifest) newRegularResourcesHash := fnv.New32a() for _, manifest := range newRelManifests { @@ -118,21 +120,21 @@ func IsReleaseUpToDate(oldRel, newRel *helmrelease.Release) (bool, error) { // Construct Helm release. func NewRelease(name, namespace string, revision int, deployType common.DeployType, resources []*spec.ResourceSpec, chart *helmchart.Chart, releaseConfig map[string]interface{}, opts ReleaseOptions) (*helmrelease.Release, error) { - if err := chartutil.ValidateReleaseName(name); err != nil { + if err := chartv2util.ValidateReleaseName(name); err != nil { return nil, fmt.Errorf("release name %q is not valid: %w", name, err) } - var status helmrelease.Status + var status helmreleasecommon.Status switch deployType { case common.DeployTypeInitial, common.DeployTypeInstall: - status = helmrelease.StatusPendingInstall + status = helmreleasecommon.StatusPendingInstall case common.DeployTypeUpgrade: - status = helmrelease.StatusPendingUpgrade + status = helmreleasecommon.StatusPendingUpgrade case common.DeployTypeRollback: - status = helmrelease.StatusPendingRollback + status = helmreleasecommon.StatusPendingRollback case common.DeployTypeUninstall: - status = helmrelease.StatusUninstalling + status = helmreleasecommon.StatusUninstalling default: panic("unexpected deploy type") } @@ -203,7 +205,7 @@ func NewRelease(name, namespace string, revision int, deployType common.DeployTy // Constructs ResourceSpecs from a Release object. func ReleaseToResourceSpecs(rel *helmrelease.Release, releaseNamespace string, noCleanNullFields bool /* TODO(major): get rid */) ([]*spec.ResourceSpec, error) { var resources []*spec.ResourceSpec - for _, manifest := range releaseutil.SplitManifestsToSlice(rel.UnstoredManifest) { + for _, manifest := range util.SplitManifests(rel.UnstoredManifest) { if res, err := spec.NewResourceSpecFromManifest(manifest, releaseNamespace, spec.ResourceSpecOptions{ StoreAs: common.StoreAsNone, LegacyNoCleanNullFields: noCleanNullFields, @@ -214,7 +216,7 @@ func ReleaseToResourceSpecs(rel *helmrelease.Release, releaseNamespace string, n } } - for _, manifest := range releaseutil.SplitManifestsToSlice(rel.Manifest) { + for _, manifest := range util.SplitManifests(rel.Manifest) { if res, err := spec.NewResourceSpecFromManifest(manifest, releaseNamespace, spec.ResourceSpecOptions{ StoreAs: common.StoreAsRegular, LegacyNoCleanNullFields: noCleanNullFields, diff --git a/pkg/release/release_storage.go b/pkg/release/release_storage.go index 2bb2a654..3b1782b4 100644 --- a/pkg/release/release_storage.go +++ b/pkg/release/release_storage.go @@ -6,17 +6,15 @@ import ( "k8s.io/client-go/kubernetes" - helmaction "github.com/werf/nelm/pkg/helm/pkg/action" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + "github.com/werf/nelm/pkg/common" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" helmstorage "github.com/werf/nelm/pkg/helm/pkg/storage" helmdriver "github.com/werf/nelm/pkg/helm/pkg/storage/driver" "github.com/werf/nelm/pkg/kube" - "github.com/werf/nelm/pkg/log" ) -var _ ReleaseStorager = (*helmstorage.Storage)(nil) +var _ ReleaseStorager = (*storageAdapter)(nil) -// Minimal interface for Helm storage drivers. type ReleaseStorager interface { Create(rls *helmrelease.Release) error Update(rls *helmrelease.Release) error @@ -24,51 +22,104 @@ type ReleaseStorager interface { Query(labels map[string]string) ([]*helmrelease.Release, error) } +type storageAdapter struct { + storage *helmstorage.Storage +} + +func (a *storageAdapter) Create(rls *helmrelease.Release) error { + if err := a.storage.Create(rls); err != nil { + return fmt.Errorf("create release: %w", err) + } + + return nil +} + +func (a *storageAdapter) Delete(name string, version int) (*helmrelease.Release, error) { + rel, err := a.storage.Delete(name, version) + if err != nil { + return nil, fmt.Errorf("delete release: %w", err) + } + + r, ok := rel.(*helmrelease.Release) + if !ok { + return nil, fmt.Errorf("unexpected release type: %T", rel) + } + + return r, nil +} + +func (a *storageAdapter) Query(labels map[string]string) ([]*helmrelease.Release, error) { + releasers, err := a.storage.Query(labels) + if err != nil { + return nil, fmt.Errorf("query releases: %w", err) + } + + result := make([]*helmrelease.Release, 0, len(releasers)) + for _, rel := range releasers { + r, ok := rel.(*helmrelease.Release) + if !ok { + return nil, fmt.Errorf("unexpected release type: %T", rel) + } + + result = append(result, r) + } + + return result, nil +} + +func (a *storageAdapter) Storage() *helmstorage.Storage { + return a.storage +} + +func (a *storageAdapter) Update(rls *helmrelease.Release) error { + if err := a.storage.Update(rls); err != nil { + return fmt.Errorf("update release: %w", err) + } + + return nil +} + type ReleaseStorageOptions struct { HistoryLimit int SQLConnection string } -// Constructs Helm release storage driver. -func NewReleaseStorage(ctx context.Context, namespace, storageDriver string, clientFactory kube.ClientFactorier, opts ReleaseStorageOptions) (*helmstorage.Storage, error) { +func NewReleaseStorage(ctx context.Context, namespace, storageDriver string, clientFactory kube.ClientFactorier, opts ReleaseStorageOptions) (ReleaseStorager, error) { var storage *helmstorage.Storage - lazyClient := helmaction.NewLazyClient(namespace, func() (*kubernetes.Clientset, error) { - return clientFactory.Static().(*kubernetes.Clientset), nil - }) + switch storageDriver { + case common.ReleaseStorageDriverSecret, common.ReleaseStorageDriverSecrets, common.ReleaseStorageDriverDefault: + if clientFactory == nil { + return nil, fmt.Errorf("kube client factory is required for %q storage driver", storageDriver) + } - logFn := func(format string, a ...interface{}) { - log.Default.Debug(ctx, format, a...) - } + clientset := clientFactory.Static().(*kubernetes.Clientset) + d := helmdriver.NewSecrets(clientset.CoreV1().Secrets(namespace)) + storage = helmstorage.Init(d) + case common.ReleaseStorageDriverConfigMap, common.ReleaseStorageDriverConfigMaps: + if clientFactory == nil { + return nil, fmt.Errorf("kube client factory is required for %q storage driver", storageDriver) + } - switch storageDriver { - case "secret", "secrets", "": - driver := helmdriver.NewSecrets(helmaction.NewSecretClient(lazyClient)) - driver.Log = logFn - - storage = helmstorage.Init(driver) - case "configmap", "configmaps": - driver := helmdriver.NewConfigMaps(helmaction.NewConfigMapClient(lazyClient)) - driver.Log = logFn - - storage = helmstorage.Init(driver) - case "memory": - driver := helmdriver.NewMemory() - driver.SetNamespace(namespace) - - storage = helmstorage.Init(driver) - case "sql": - driver, err := helmdriver.NewSQL(opts.SQLConnection, logFn, namespace) + clientset := clientFactory.Static().(*kubernetes.Clientset) + d := helmdriver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace)) + storage = helmstorage.Init(d) + case common.ReleaseStorageDriverMemory: + d := helmdriver.NewMemory() + d.SetNamespace(namespace) + storage = helmstorage.Init(d) + case common.ReleaseStorageDriverSQL: + d, err := helmdriver.NewSQL(opts.SQLConnection, namespace) if err != nil { return nil, fmt.Errorf("construct sql driver: %w", err) } - storage = helmstorage.Init(driver) + storage = helmstorage.Init(d) default: panic(fmt.Sprintf("Unknown storage driver: %s", storageDriver)) } storage.MaxHistory = opts.HistoryLimit - return storage, nil + return &storageAdapter{storage: storage}, nil } diff --git a/pkg/resource/helpers_ai_test.go b/pkg/resource/helpers_ai_test.go index 3d56ee87..2ab2272a 100644 --- a/pkg/resource/helpers_ai_test.go +++ b/pkg/resource/helpers_ai_test.go @@ -16,7 +16,6 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/resource" "github.com/werf/nelm/pkg/resource/spec" ) @@ -150,6 +149,4 @@ func setupLocalSchemaDir(t *testing.T, schemas map[string]string) string { func setupTestEnvironment(t *testing.T) { t.Helper() - common.APIResourceValidationJSONSchemasCacheDir = t.TempDir() - featgate.FeatGateResourceValidation.Enable() } diff --git a/pkg/resource/kubeconform.go b/pkg/resource/kubeconform.go index 43515625..3d289e5c 100644 --- a/pkg/resource/kubeconform.go +++ b/pkg/resource/kubeconform.go @@ -24,6 +24,7 @@ import ( "sigs.k8s.io/yaml" "github.com/werf/nelm/pkg/common" + "github.com/werf/nelm/pkg/helm/pkg/helmpath" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/resource/spec" "github.com/werf/nelm/pkg/util" @@ -453,7 +454,7 @@ func createKubeConformCacheDir(subDir, source string) (string, error) { sourceDirName = u.Hostname() + "-" + sourceHash[:7] } - path := filepath.Join(common.APIResourceValidationJSONSchemasCacheDir, subDir, sourceDirName) + path := filepath.Join(helmpath.CachePath(common.CacheDirAPIResourceJSONSchemas), subDir, sourceDirName) if stat, err := os.Stat(path); os.IsNotExist(err) { if err := os.MkdirAll(path, 0o755); err != nil { diff --git a/pkg/resource/metadata.go b/pkg/resource/metadata.go index 256df2f2..02c1bf7d 100644 --- a/pkg/resource/metadata.go +++ b/pkg/resource/metadata.go @@ -18,9 +18,9 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" + "github.com/werf/kubedog/pkg/dyntracker/statestore" "github.com/werf/nelm/pkg/common" - helmrelease "github.com/werf/nelm/pkg/helm/pkg/release" + helmrelease "github.com/werf/nelm/pkg/helm/pkg/release/v1" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/resource/spec" "github.com/werf/nelm/pkg/util" @@ -256,13 +256,13 @@ func deployConditionsForAnnotation(meta *spec.ResourceMeta, annoPattern *regexp. result[common.InstallOnDelete] = append(result[common.InstallOnDelete], common.StagePostInstall) case string(helmrelease.HookTest), "test-success": result[common.InstallOnTest] = append(result[common.InstallOnTest], common.StageInstall) - case string(helmrelease.HookInstall): + case string(common.InstallOnInstall): result[common.InstallOnInstall] = append(result[common.InstallOnInstall], common.StageInstall) - case string(helmrelease.HookUpgrade): + case string(common.InstallOnUpgrade): result[common.InstallOnUpgrade] = append(result[common.InstallOnUpgrade], common.StageInstall) - case string(helmrelease.HookRollback): + case string(common.InstallOnRollback): result[common.InstallOnRollback] = append(result[common.InstallOnRollback], common.StageInstall) - case string(helmrelease.HookDelete): + case string(common.InstallOnDelete): result[common.InstallOnDelete] = append(result[common.InstallOnDelete], common.StageInstall) default: panic(fmt.Sprintf("unknown value %q for %s", value, key)) @@ -321,13 +321,13 @@ func externalDeps(resMeta *spec.ResourceMeta, releaseNamespace string) map[strin return deps } -func failMode(meta *spec.ResourceMeta) multitrack.FailMode { +func failMode(meta *spec.ResourceMeta) statestore.FailMode { _, value, found := spec.FindAnnotationOrLabelByKeyPattern(meta.Annotations, common.AnnotationKeyPatternFailMode) if !found { - return multitrack.FailWholeDeployProcessImmediately + return statestore.FailWholeDeployProcessImmediately } - return multitrack.FailMode(value) + return statestore.FailMode(value) } func failuresAllowed(unstruct *unstructured.Unstructured) int { @@ -534,49 +534,6 @@ func manualInternalDeployDependencies(meta *spec.ResourceMeta) []*InternalDepend deps := map[string]*InternalDependency{} - if annotations, found := spec.FindAnnotationsOrLabelsByKeyPattern(meta.Annotations, common.AnnotationKeyPatternDependency); found { - for key, value := range annotations { - matches := common.AnnotationKeyPatternDependency.FindStringSubmatch(key) - idSubexpIndex := common.AnnotationKeyPatternDependency.SubexpIndex("id") - depID := matches[idSubexpIndex] - valParts := strings.Split(value, ":") - depAPIVersionParts := strings.SplitN(valParts[0], "/", 2) - - var gvk schema.GroupVersionKind - if len(depAPIVersionParts) == 1 { - gvk = schema.GroupVersionKind{ - Version: depAPIVersionParts[0], - Kind: valParts[1], - } - } else { - gvk = schema.GroupVersionKind{ - Group: depAPIVersionParts[0], - Version: depAPIVersionParts[1], - Kind: valParts[1], - } - } - - var depNamespace string - if len(valParts) == 4 { - depNamespace = valParts[2] - } - - depName := valParts[len(valParts)-1] - - dep := &InternalDependency{ - ResourceMatcher: &spec.ResourceMatcher{ - Names: []string{depName}, - Namespaces: []string{depNamespace}, - Groups: []string{gvk.Group}, - Versions: []string{gvk.Version}, - Kinds: []string{gvk.Kind}, - }, - ResourceState: common.ResourceStatePresent, - } - deps[depID] = dep - } - } - if annotations, found := spec.FindAnnotationsOrLabelsByKeyPattern(meta.Annotations, common.AnnotationKeyPatternDeployDependency); found { for key, value := range annotations { matches := common.AnnotationKeyPatternDeployDependency.FindStringSubmatch(key) @@ -749,13 +706,13 @@ func skipLogsForContainers(meta *spec.ResourceMeta) []string { return containers } -func trackTerminationMode(meta *spec.ResourceMeta) multitrack.TrackTerminationMode { +func trackTerminationMode(meta *spec.ResourceMeta) statestore.TrackTerminationMode { _, value, found := spec.FindAnnotationOrLabelByKeyPattern(meta.Annotations, common.AnnotationKeyPatternTrackTerminationMode) if !found { - return multitrack.WaitUntilResourceReady + return statestore.WaitUntilResourceReady } - return multitrack.TrackTerminationMode(value) + return statestore.TrackTerminationMode(value) } func validateDeleteDependencies(meta *spec.ResourceMeta) error { @@ -979,10 +936,10 @@ func validateDeployOn(meta *spec.ResourceMeta) error { string(helmrelease.HookPreDelete), string(helmrelease.HookPostDelete), string(helmrelease.HookTest), - string(helmrelease.HookInstall), - string(helmrelease.HookUpgrade), - string(helmrelease.HookRollback), - string(helmrelease.HookDelete), + string(common.InstallOnInstall), + string(common.InstallOnUpgrade), + string(common.InstallOnRollback), + string(common.InstallOnDelete), "test-success": default: return fmt.Errorf("value %q for annotation %q is not supported", value, key) @@ -1117,36 +1074,6 @@ func validateHook(meta *spec.ResourceMeta) error { return nil } -func validateInternalDependencies(meta *spec.ResourceMeta) error { - if annotations, found := spec.FindAnnotationsOrLabelsByKeyPattern(meta.Annotations, common.AnnotationKeyPatternDependency); found { - for key, value := range annotations { - keyMatches := common.AnnotationKeyPatternDependency.FindStringSubmatch(key) - if keyMatches == nil { - return fmt.Errorf("invalid key for annotation %q", key) - } - - idSubexpIndex := common.AnnotationKeyPatternDependency.SubexpIndex("id") - if idSubexpIndex == -1 { - return fmt.Errorf("invalid regexp pattern %q for annotation %q", common.AnnotationKeyPatternDependency.String(), key) - } - - if len(keyMatches) < idSubexpIndex+1 { - return fmt.Errorf("can't parse dependency id from annotation key %q", key) - } - - if value != "" { - valueElems := strings.Split(value, ":") - - if len(valueElems) != 3 && len(valueElems) != 4 { - return fmt.Errorf(`invalid format of value %q for annotation %q, should be: apiVersion:kind[:namespace]:name or empty`, value, key) - } - } - } - } - - return nil -} - func validateOwnership(meta *spec.ResourceMeta) error { if key, value, found := spec.FindAnnotationOrLabelByKeyPattern(meta.Annotations, common.AnnotationKeyPatternOwnership); found { if value == "" { @@ -1225,9 +1152,9 @@ func validateTrack(meta *spec.ResourceMeta) error { } switch value { - case string(multitrack.IgnoreAndContinueDeployProcess): - case string(multitrack.FailWholeDeployProcessImmediately): - case string(multitrack.LegacyHopeUntilEndOfDeployProcess): + case string(statestore.IgnoreAndContinueDeployProcess): + case string(statestore.FailWholeDeployProcessImmediately): + case string(statestore.LegacyHopeUntilEndOfDeployProcess): default: return fmt.Errorf("invalid unknown value %q for annotation %q", value, key) } @@ -1431,8 +1358,8 @@ func validateTrack(meta *spec.ResourceMeta) error { } switch value { - case string(multitrack.WaitUntilResourceReady): - case string(multitrack.NonBlocking): + case string(statestore.WaitUntilResourceReady): + case string(statestore.NonBlocking): default: return fmt.Errorf("invalid unknown value %q for annotation %q", value, key) } diff --git a/pkg/resource/resource.go b/pkg/resource/resource.go index 6955f671..06d40955 100644 --- a/pkg/resource/resource.go +++ b/pkg/resource/resource.go @@ -11,7 +11,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" + "github.com/werf/kubedog/pkg/dyntracker/statestore" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/resource/spec" @@ -30,7 +30,7 @@ type InstallableResource struct { DeleteOnSucceeded bool `json:"deleteOnSucceeded"` DeleteOnFailed bool `json:"deleteOnFailed"` KeepOnDelete bool `json:"keepOnDelete"` - FailMode multitrack.FailMode `json:"failMode"` + FailMode statestore.FailMode `json:"failMode"` FailuresAllowed int `json:"failuresAllowed"` IgnoreReadinessProbeFailsForContainers map[string]time.Duration `json:"ignoreReadinessProbeFailsForContainers,omitempty"` LogRegex *regexp.Regexp `json:"logRegex"` @@ -43,7 +43,7 @@ type InstallableResource struct { SkipLogsForContainers []string `json:"skipLogsForContainers,omitempty"` SkipLogsRegex *regexp.Regexp `json:"skipLogsRegex"` SkipLogsRegexForContainers map[string]*regexp.Regexp `json:"skipLogsRegexForContainers"` - TrackTerminationMode multitrack.TrackTerminationMode `json:"trackTerminationMode"` + TrackTerminationMode statestore.TrackTerminationMode `json:"trackTerminationMode"` Weight *int `json:"weight,omitempty"` ManualInternalDependencies []*InternalDependency `json:"manualInternalDependencies,omitempty"` AutoInternalDependencies []*InternalDependency `json:"autoInternalDependencies,omitempty"` @@ -83,10 +83,6 @@ func NewInstallableResource(res *spec.ResourceSpec, releaseNamespace string, cli return nil, fmt.Errorf("validate deploy dependencies: %w", err) } - if err := validateInternalDependencies(res.ResourceMeta); err != nil { - return nil, fmt.Errorf("validate internal dependencies: %w", err) - } - if err := validateExternalDependencies(res.ResourceMeta); err != nil { return nil, fmt.Errorf("validate external dependencies: %w", err) } diff --git a/pkg/resource/resource_test.go b/pkg/resource/resource_test.go index 12065fe8..3958352c 100644 --- a/pkg/resource/resource_test.go +++ b/pkg/resource/resource_test.go @@ -14,7 +14,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" + "github.com/werf/kubedog/pkg/dyntracker/statestore" "github.com/werf/nelm/pkg/common" "github.com/werf/nelm/pkg/kube" "github.com/werf/nelm/pkg/kube/fake" @@ -874,7 +874,7 @@ func (s *InstallableResourceSuite) TestNewInstallableResourceForTracking() { { expect: func(resSpec *spec.ResourceSpec) *resource.InstallableResource { res := defaultInstallableResource(resSpec) - res.FailMode = multitrack.IgnoreAndContinueDeployProcess + res.FailMode = statestore.IgnoreAndContinueDeployProcess return res }, @@ -974,7 +974,7 @@ func (s *InstallableResourceSuite) TestNewInstallableResourceForTracking() { { expect: func(resSpec *spec.ResourceSpec) *resource.InstallableResource { res := defaultInstallableResource(resSpec) - res.TrackTerminationMode = multitrack.NonBlocking + res.TrackTerminationMode = statestore.NonBlocking return res }, @@ -1207,10 +1207,10 @@ func defaultInstallableResource(resSpec *spec.ResourceSpec) *resource.Installabl return &resource.InstallableResource{ ResourceSpec: resSpec, Ownership: common.OwnershipRelease, - FailMode: multitrack.FailWholeDeployProcessImmediately, + FailMode: statestore.FailWholeDeployProcessImmediately, NoActivityTimeout: 4 * time.Minute, ShowLogsOnlyForNumberOfReplicas: 1, - TrackTerminationMode: multitrack.WaitUntilResourceReady, + TrackTerminationMode: statestore.WaitUntilResourceReady, Weight: lo.ToPtr(0), DeployConditions: map[common.On][]common.Stage{ common.InstallOnInstall: {common.StageInstall}, diff --git a/pkg/resource/sensitive.go b/pkg/resource/sensitive.go index 27806d25..a08e5b00 100644 --- a/pkg/resource/sensitive.go +++ b/pkg/resource/sensitive.go @@ -13,7 +13,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/resource/spec" ) @@ -24,16 +23,6 @@ type SensitiveInfo struct { SensitivePaths []string } -func (i *SensitiveInfo) FullySensitive() bool { - return i.IsSensitive && len(i.SensitivePaths) == 1 && i.SensitivePaths[0] == HideAll -} - -func IsSensitive(groupKind schema.GroupKind, annotations map[string]string) bool { - info := GetSensitiveInfo(groupKind, annotations) - - return info.IsSensitive -} - func GetSensitiveInfo(groupKind schema.GroupKind, annotations map[string]string) SensitiveInfo { // Check for werf.io/sensitive-paths (comma-separated) if _, value, found := spec.FindAnnotationOrLabelByKeyPattern(annotations, common.AnnotationKeyPatternSensitivePaths); found { @@ -43,33 +32,18 @@ func GetSensitiveInfo(groupKind schema.GroupKind, annotations map[string]string) } } - useNewBehavior := featgate.FeatGateFieldSensitive.Enabled() || featgate.FeatGatePreviewV2.Enabled() - // Check for werf.io/sensitive annotation if _, value, found := spec.FindAnnotationOrLabelByKeyPattern(annotations, common.AnnotationKeyPatternSensitive); found { - sensitive := lo.Must(strconv.ParseBool(value)) - if sensitive { - if useNewBehavior { - // V2 behavior: only hide data.* and stringData.* - return SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}} - } else { - // V1 behavior: hide everything - return SensitiveInfo{IsSensitive: true, SensitivePaths: []string{HideAll}} - } - } else { - return SensitiveInfo{IsSensitive: false, SensitivePaths: nil} + if lo.Must(strconv.ParseBool(value)) { + return SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}} } + + return SensitiveInfo{IsSensitive: false, SensitivePaths: nil} } // Default behavior for Secrets if groupKind == (schema.GroupKind{Group: "", Kind: "Secret"}) { - if useNewBehavior { - // V2 behavior: only hide data.* and stringData.* - return SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}} - } else { - // V1 behavior: hide everything - return SensitiveInfo{IsSensitive: true, SensitivePaths: []string{HideAll}} - } + return SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}} } return SensitiveInfo{IsSensitive: false, SensitivePaths: nil} @@ -122,6 +96,7 @@ func RedactSensitiveData(unstruct *unstructured.Unstructured, sensitivePaths []s func redactSensitiveData(unstruct *unstructured.Unstructured, sensitivePaths []string) *unstructured.Unstructured { for _, pathExpr := range sensitivePaths { + // TODO(major): should we remove this? if pathExpr == HideAll { return &unstructured.Unstructured{Object: map[string]interface{}{ "apiVersion": unstruct.GetAPIVersion(), diff --git a/pkg/resource/sensitive_test.go b/pkg/resource/sensitive_test.go index 3305641e..a9374a57 100644 --- a/pkg/resource/sensitive_test.go +++ b/pkg/resource/sensitive_test.go @@ -1,114 +1,67 @@ package resource_test import ( - "os" - "strings" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/resource" ) func TestGetSensitiveInfo(t *testing.T) { - // Save original env and restore after test - originalEnv := os.Getenv(featgate.FeatGateFieldSensitive.EnvVarName()) - defer func() { - if originalEnv != "" { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), originalEnv) - } - }() - tests := []struct { - name string - enableFeature bool - groupKind schema.GroupKind - annotations map[string]string - expected resource.SensitiveInfo + name string + groupKind schema.GroupKind + annotations map[string]string + expected resource.SensitiveInfo }{ { - name: "regular resource not sensitive", - enableFeature: true, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, - annotations: map[string]string{}, - expected: resource.SensitiveInfo{IsSensitive: false, SensitivePaths: nil}, + name: "regular resource not sensitive", + groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + annotations: map[string]string{}, + expected: resource.SensitiveInfo{IsSensitive: false, SensitivePaths: nil}, }, { - name: "secret resource automatically sensitive - legacy behavior", - enableFeature: false, - groupKind: schema.GroupKind{Group: "", Kind: "Secret"}, - annotations: map[string]string{}, - expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"$$HIDE_ALL$$"}}, + name: "secret with sensitive annotation set to false", + groupKind: schema.GroupKind{Group: "", Kind: "Secret"}, + annotations: map[string]string{"werf.io/sensitive": "false"}, + expected: resource.SensitiveInfo{IsSensitive: false, SensitivePaths: nil}, }, { - name: "secret resource with annotation - legacy behavior", - enableFeature: false, - groupKind: schema.GroupKind{Group: "", Kind: "Secret"}, - annotations: map[string]string{ - "werf.io/sensitive": "false", - }, - expected: resource.SensitiveInfo{IsSensitive: false, SensitivePaths: nil}, + name: "secret resource automatically sensitive", + groupKind: schema.GroupKind{Group: "", Kind: "Secret"}, + annotations: map[string]string{}, + expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}}, }, { - name: "secret with sensitive annotation set to false", - enableFeature: true, - groupKind: schema.GroupKind{Group: "", Kind: "Secret"}, - annotations: map[string]string{ - "werf.io/sensitive": "false", - }, - expected: resource.SensitiveInfo{IsSensitive: false, SensitivePaths: nil}, - }, - { - name: "secret resource automatically sensitive - new behavior", - enableFeature: true, - groupKind: schema.GroupKind{Group: "", Kind: "Secret"}, - annotations: map[string]string{}, - expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}}, - }, - { - name: "resource with sensitive annotation set to true - legacy behavior", - enableFeature: false, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, - annotations: map[string]string{ - "werf.io/sensitive": "true", - }, - expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{resource.HideAll}}, - }, - { - name: "resource with sensitive annotation set to true - new behavior", - enableFeature: true, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + name: "resource with sensitive annotation set to true", + groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, annotations: map[string]string{ "werf.io/sensitive": "true", }, expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.*", "stringData.*"}}, }, { - name: "resource with comma-separated sensitive-paths annotation", - enableFeature: true, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + name: "resource with comma-separated sensitive-paths annotation", + groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, annotations: map[string]string{ "werf.io/sensitive-paths": "spec.template.spec.containers.*.env.*.value,data.password", }, expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"spec.template.spec.containers.*.env.*.value", "data.password"}}, }, { - name: "resource with escaped comma in sensitive-paths", - enableFeature: true, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + name: "resource with escaped comma in sensitive-paths", + groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, annotations: map[string]string{ "werf.io/sensitive-paths": "data.field\\,with\\,commas,spec.other", }, expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.field,with,commas", "spec.other"}}, }, { - name: "resource with both sensitive and sensitive-paths annotations - sensitive path precedence in v2", - enableFeature: true, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + name: "resource with both sensitive and sensitive-paths annotations - sensitive path precedence in v2", + groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, annotations: map[string]string{ "werf.io/sensitive": "true", "werf.io/sensitive-paths": "data.password", @@ -116,32 +69,17 @@ func TestGetSensitiveInfo(t *testing.T) { expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"data.password"}}, }, { - name: "resource with empty sensitive-paths annotation", - enableFeature: true, - groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, + name: "resource with empty sensitive-paths annotation", + groupKind: schema.GroupKind{Group: "apps", Kind: "Deployment"}, annotations: map[string]string{ "werf.io/sensitive-paths": "", }, expected: resource.SensitiveInfo{IsSensitive: false, SensitivePaths: nil}, }, - { - name: "resource with sensitive-paths annotation - feature flag disabled", - enableFeature: false, - groupKind: schema.GroupKind{Group: "v1", Kind: "ConfigMap"}, - annotations: map[string]string{ - "werf.io/sensitive-paths": "$.data[*]", - }, - expected: resource.SensitiveInfo{IsSensitive: true, SensitivePaths: []string{"$.data[*]"}}, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Set feature gate - if tt.enableFeature { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), "true") - } - result := resource.GetSensitiveInfo(tt.groupKind, tt.annotations) assert.Equal(t, tt.expected, result, "behavior should match expected") @@ -206,16 +144,6 @@ func TestParseSensitivePaths(t *testing.T) { } func TestRedactAtJSONPath(t *testing.T) { - // Enable feature gate - originalEnv := os.Getenv(featgate.FeatGateFieldSensitive.EnvVarName()) - defer func() { - if originalEnv != "" { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), originalEnv) - } - }() - - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), "true") - tests := []struct { name string input *unstructured.Unstructured @@ -585,61 +513,14 @@ func TestRedactAtJSONPath(t *testing.T) { } func TestRedactSensitiveData(t *testing.T) { - // Save original env and restore after test - originalEnv := os.Getenv(featgate.FeatGateFieldSensitive.EnvVarName()) - defer func() { - if originalEnv != "" { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), originalEnv) - } - }() - tests := []struct { name string - enableFeature bool input *unstructured.Unstructured sensitivePaths []string checkFunc func(t *testing.T, result *unstructured.Unstructured) }{ { - name: "bug: sensitive-paths ignored when feature flag disabled", - enableFeature: false, - input: &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "test-config", - "namespace": "default", - }, - "data": map[string]interface{}{ - "key1": "sensitive-value-1", - "key2": "sensitive-value-2", - }, - }, - }, - sensitivePaths: []string{"data.*"}, - checkFunc: func(t *testing.T, result *unstructured.Unstructured) { - // The bug is that when feature flag is disabled, the entire data section - // is removed instead of redacting only the specified sensitive paths - data, found, err := unstructured.NestedMap(result.Object, "data") - require.NoError(t, err) - - if !found { - t.Errorf("data section was completely removed instead of being redacted") - } else { - // If data exists, it should be redacted - for key, value := range data { - valueStr, ok := value.(string) - if ok && !strings.Contains(valueStr, "sensitive") { - t.Errorf("Expected data.%s to be redacted but got: %s", key, valueStr) - } - } - } - }, - }, - { - name: "no sensitive paths", - enableFeature: true, + name: "no sensitive paths", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", @@ -662,8 +543,7 @@ func TestRedactSensitiveData(t *testing.T) { }, }, { - name: "hide all with feature gate", - enableFeature: true, + name: "hide all", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", @@ -689,8 +569,7 @@ func TestRedactSensitiveData(t *testing.T) { }, }, { - name: "redact data fields with wildcard - new behavior", - enableFeature: true, + name: "redact data fields with wildcard", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", @@ -723,8 +602,7 @@ func TestRedactSensitiveData(t *testing.T) { }, }, { - name: "redact specific field", - enableFeature: true, + name: "redact specific field", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "v1", @@ -753,8 +631,7 @@ func TestRedactSensitiveData(t *testing.T) { }, }, { - name: "type change handling - string to slice", - enableFeature: true, + name: "type change handling - string to slice", input: &unstructured.Unstructured{ Object: map[string]interface{}{ "apiVersion": "apps/v1", @@ -797,11 +674,6 @@ func TestRedactSensitiveData(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Set feature gate - if tt.enableFeature { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), "true") - } - result := resource.RedactSensitiveData(tt.input, tt.sensitivePaths) // Ensure original object is not modified @@ -813,16 +685,6 @@ func TestRedactSensitiveData(t *testing.T) { } func TestRedactSensitiveDataEdgeCases(t *testing.T) { - // Enable feature gate - originalEnv := os.Getenv(featgate.FeatGateFieldSensitive.EnvVarName()) - defer func() { - if originalEnv != "" { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), originalEnv) - } - }() - - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), "true") - tests := []struct { name string input *unstructured.Unstructured @@ -867,16 +729,6 @@ func TestRedactSensitiveDataEdgeCases(t *testing.T) { } func TestSHA256HashingConsistency(t *testing.T) { - // Enable feature gate - originalEnv := os.Getenv(featgate.FeatGateFieldSensitive.EnvVarName()) - defer func() { - if originalEnv != "" { - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), originalEnv) - } - }() - - t.Setenv(featgate.FeatGateFieldSensitive.EnvVarName(), "true") - input1 := &unstructured.Unstructured{ Object: map[string]interface{}{ "data": map[string]interface{}{ diff --git a/pkg/resource/spec/patch.go b/pkg/resource/spec/patch.go index ec2540b8..8eda3534 100644 --- a/pkg/resource/spec/patch.go +++ b/pkg/resource/spec/patch.go @@ -9,7 +9,7 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/werf/kubedog/pkg/trackers/rollout/multitrack" + "github.com/werf/kubedog/pkg/dyntracker/statestore" "github.com/werf/nelm/pkg/common" ) @@ -127,8 +127,8 @@ func (p *LegacyOnlyTrackJobsPatcher) Match(ctx context.Context, info *ResourcePa func (p *LegacyOnlyTrackJobsPatcher) Patch(ctx context.Context, info *ResourcePatcherResourceInfo) (*unstructured.Unstructured, error) { annos := map[string]string{} - annos["werf.io/fail-mode"] = string(multitrack.IgnoreAndContinueDeployProcess) - annos["werf.io/track-termination-mode"] = string(multitrack.NonBlocking) + annos["werf.io/fail-mode"] = string(statestore.IgnoreAndContinueDeployProcess) + annos["werf.io/track-termination-mode"] = string(statestore.NonBlocking) setAnnotationsAndLabels(info.Obj, annos, nil) diff --git a/pkg/resource/spec/resource_spec.go b/pkg/resource/spec/resource_spec.go index 92ec9ed6..1cae4a22 100644 --- a/pkg/resource/spec/resource_spec.go +++ b/pkg/resource/spec/resource_spec.go @@ -10,7 +10,6 @@ import ( "k8s.io/client-go/kubernetes/scheme" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" ) // Contains all generic information about the resource, e.g. its name, namespace, GVK and its spec. @@ -25,7 +24,7 @@ type ResourceSpec struct { func NewResourceSpec(unstruct *unstructured.Unstructured, releaseNamespace string, opts ResourceSpecOptions) *ResourceSpec { unstruct = CleanUnstruct(unstruct, CleanUnstructOptions{ - CleanNullFields: (featgate.FeatGatePreviewV2.Enabled() || featgate.FeatGateCleanNullFields.Enabled()) && !opts.LegacyNoCleanNullFields, + CleanNullFields: !opts.LegacyNoCleanNullFields, }) if opts.StoreAs == "" { diff --git a/pkg/resource/spec/sort.go b/pkg/resource/spec/sort.go index 1237797d..5f104724 100644 --- a/pkg/resource/spec/sort.go +++ b/pkg/resource/spec/sort.go @@ -1,21 +1,11 @@ package spec -import "github.com/werf/nelm/pkg/common" - func ResourceSpecSortHandler(r1, r2 *ResourceSpec) bool { - sortAs1 := r1.StoreAs - sortAs2 := r2.StoreAs - - // TODO(major): sorted based on sortAs for compatibility. In future should just probably sort - // like this: first CRDs (any type), then helm.sh/hook hooks, then the rest - if sortAs1 != sortAs2 { - if sortAs1 == common.StoreAsNone { - return true - } else if sortAs1 == common.StoreAsHook && sortAs2 != common.StoreAsNone { - return true - } else { - return false - } + isCRD1 := IsCRD(r1.GroupVersionKind.GroupKind()) + isCRD2 := IsCRD(r2.GroupVersionKind.GroupKind()) + + if isCRD1 != isCRD2 { + return isCRD1 } return ResourceMetaSortHandler(r1.ResourceMeta, r2.ResourceMeta) diff --git a/pkg/resource/spec/unstruct.go b/pkg/resource/spec/unstruct.go index a2708e83..f54974e6 100644 --- a/pkg/resource/spec/unstruct.go +++ b/pkg/resource/spec/unstruct.go @@ -3,7 +3,7 @@ package spec import ( "regexp" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "github.com/werf/nelm/pkg/common" diff --git a/pkg/resource/validate.go b/pkg/resource/validate.go index caf35424..8d58afd2 100644 --- a/pkg/resource/validate.go +++ b/pkg/resource/validate.go @@ -11,7 +11,6 @@ import ( "k8s.io/client-go/kubernetes/scheme" "github.com/werf/nelm/pkg/common" - "github.com/werf/nelm/pkg/featgate" "github.com/werf/nelm/pkg/log" "github.com/werf/nelm/pkg/resource/spec" "github.com/werf/nelm/pkg/util" @@ -25,7 +24,7 @@ func ValidateLocal(ctx context.Context, releaseNamespace string, transformedReso return fmt.Errorf("validate for no duplicated resources: %w", err) } - if featgate.FeatGateResourceValidation.Enabled() && !opts.NoResourceValidation { + if !opts.NoResourceValidation { if err := validateResourceSchemas(ctx, releaseNamespace, transformedResources, opts); err != nil { return fmt.Errorf("validate resource schemas: %w", err) } diff --git a/pkg/track/progress_tables.go b/pkg/track/progress_tables.go index 07a76a65..201b90e3 100644 --- a/pkg/track/progress_tables.go +++ b/pkg/track/progress_tables.go @@ -11,12 +11,12 @@ import ( "github.com/gookit/color" prtable "github.com/jedib0t/go-pretty/v6/table" "github.com/samber/lo" - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" - "github.com/werf/kubedog/pkg/trackers/dyntracker/logstore" - "github.com/werf/kubedog/pkg/trackers/dyntracker/statestore" - kdutil "github.com/werf/kubedog/pkg/trackers/dyntracker/util" + "github.com/werf/kubedog/pkg/dyntracker/logstore" + "github.com/werf/kubedog/pkg/dyntracker/statestore" + kdutil "github.com/werf/kubedog/pkg/dyntracker/util" "github.com/werf/nelm/pkg/log" ) diff --git a/pkg/ts/deno.go b/pkg/ts/deno.go index f900509b..c64ea9de 100644 --- a/pkg/ts/deno.go +++ b/pkg/ts/deno.go @@ -15,13 +15,14 @@ import ( "github.com/samber/lo" "github.com/werf/nelm/pkg/common" - helmchart "github.com/werf/nelm/pkg/helm/pkg/chart" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + v2chart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" "github.com/werf/nelm/pkg/log" ) var chartTSEntryPoints = [...]string{common.ChartTSEntryPointTS, common.ChartTSEntryPointJS} -func BundleChartsRecursive(ctx context.Context, chart *helmchart.Chart, path string, rebuildBundle bool, binaryPath string) error { +func BundleChartsRecursive(ctx context.Context, chart *v2chart.Chart, path string, rebuildBundle bool, binaryPath string) error { if !hasTSFiles(chart) { return nil } @@ -56,7 +57,7 @@ func RunDenoInstall(ctx context.Context, chartPath, binaryPath string) error { return nil } -func bundleChartsRecursive(ctx context.Context, chart *helmchart.Chart, path string, rebuildBundle bool, denoBin string) error { +func bundleChartsRecursive(ctx context.Context, chart *v2chart.Chart, path string, rebuildBundle bool, denoBin string) error { entrypoint, bundle := getEntrypointAndBundle(chart.RuntimeFiles) if entrypoint != "" { @@ -93,13 +94,13 @@ func bundleChartsRecursive(ctx context.Context, chart *helmchart.Chart, path str return nil } -func getEntrypointAndBundle(files []*helmchart.File) (string, *helmchart.File) { +func getEntrypointAndBundle(files []*chartcommon.File) (string, *chartcommon.File) { entrypoint := findEntrypointInFiles(files) if entrypoint == "" { return "", nil } - bundleFile, foundBundle := lo.Find(files, func(f *helmchart.File) bool { + bundleFile, foundBundle := lo.Find(files, func(f *chartcommon.File) bool { return f.Name == common.ChartTSBundleFile }) @@ -110,7 +111,7 @@ func getEntrypointAndBundle(files []*helmchart.File) (string, *helmchart.File) { return entrypoint, bundleFile } -func hasTSFiles(chart *helmchart.Chart) bool { +func hasTSFiles(chart *v2chart.Chart) bool { entrypoint := findEntrypointInFiles(chart.RuntimeFiles) if entrypoint != "" { return true @@ -125,7 +126,7 @@ func hasTSFiles(chart *helmchart.Chart) bool { return false } -func findEntrypointInFiles(files []*helmchart.File) string { +func findEntrypointInFiles(files []*chartcommon.File) string { sourceFiles := make(map[string][]byte) for _, f := range files { diff --git a/pkg/ts/options.go b/pkg/ts/options.go index 50de2c88..f4500e12 100644 --- a/pkg/ts/options.go +++ b/pkg/ts/options.go @@ -3,22 +3,22 @@ package ts import ( "context" - "github.com/werf/nelm/pkg/helm/pkg/werf/helmopts" + "github.com/werf/nelm/pkg/common" ) var tsOptionsKey chartTSOptionsKey type chartTSOptionsKey struct{} -func GetTSOptionsFromContext(ctx context.Context) helmopts.TypeScriptOptions { - opts, ok := ctx.Value(tsOptionsKey).(helmopts.TypeScriptOptions) +func GetTSOptionsFromContext(ctx context.Context) common.TypeScriptOptions { + opts, ok := ctx.Value(tsOptionsKey).(common.TypeScriptOptions) if !ok { - return helmopts.TypeScriptOptions{} + return common.TypeScriptOptions{} } return opts } -func NewContextWithTSOptions(ctx context.Context, opts helmopts.TypeScriptOptions) context.Context { +func NewContextWithTSOptions(ctx context.Context, opts common.TypeScriptOptions) context.Context { return context.WithValue(ctx, tsOptionsKey, opts) } diff --git a/pkg/ts/render.go b/pkg/ts/render.go index a179ab29..d875ce91 100644 --- a/pkg/ts/render.go +++ b/pkg/ts/render.go @@ -12,12 +12,12 @@ import ( "sigs.k8s.io/yaml" "github.com/werf/nelm/pkg/common" - helmchart "github.com/werf/nelm/pkg/helm/pkg/chart" - "github.com/werf/nelm/pkg/helm/pkg/chartutil" + chartcommon "github.com/werf/nelm/pkg/helm/pkg/chart/common" + helmchart "github.com/werf/nelm/pkg/helm/pkg/chart/v2" "github.com/werf/nelm/pkg/log" ) -func RenderChart(ctx context.Context, chart *helmchart.Chart, renderedValues chartutil.Values, rebuildBundle bool, chartPath, tempDirPath, denoBinaryPath string) (map[string]string, error) { +func RenderChart(ctx context.Context, chart *helmchart.Chart, renderedValues chartcommon.Values, rebuildBundle bool, chartPath, tempDirPath, denoBinaryPath string) (map[string]string, error) { if !hasTSFiles(chart) { return map[string]string{}, nil } @@ -39,7 +39,7 @@ func RenderChart(ctx context.Context, chart *helmchart.Chart, renderedValues cha return allRendered, nil } -func renderChartRecursive(ctx context.Context, chart *helmchart.Chart, values chartutil.Values, pathPrefix, chartPath, tempDirPath, denoBin string) (map[string]string, error) { +func renderChartRecursive(ctx context.Context, chart *helmchart.Chart, values chartcommon.Values, pathPrefix, chartPath, tempDirPath, denoBin string) (map[string]string, error) { log.Default.Debug(ctx, "Rendering TypeScript for chart %q (path prefix: %s)", chart.Name(), pathPrefix) results := make(map[string]string) @@ -80,7 +80,7 @@ func renderChartRecursive(ctx context.Context, chart *helmchart.Chart, values ch return results, nil } -func renderChart(ctx context.Context, bundle *helmchart.File, chart *helmchart.Chart, renderedValues chartutil.Values, tempDirPath, denoBin string) (string, error) { +func renderChart(ctx context.Context, bundle *chartcommon.File, chart *helmchart.Chart, renderedValues chartcommon.Values, tempDirPath, denoBin string) (string, error) { renderDir := filepath.Join(tempDirPath, "typescript-render", chart.ChartFullPath()) if err := os.MkdirAll(renderDir, 0o755); err != nil { return "", fmt.Errorf("create temp dir for render context: %w", err) @@ -102,8 +102,8 @@ func renderChart(ctx context.Context, bundle *helmchart.File, chart *helmchart.C return strings.TrimSpace(string(resultBytes)), nil } -func scopeValuesForSubchart(parentValues chartutil.Values, subchartName string, subchart *helmchart.Chart) chartutil.Values { - scoped := chartutil.Values{ +func scopeValuesForSubchart(parentValues chartcommon.Values, subchartName string, subchart *helmchart.Chart) chartcommon.Values { + scoped := chartcommon.Values{ "Chart": buildChartMetadata(subchart), "Values": map[string]any{}, } @@ -119,7 +119,7 @@ func scopeValuesForSubchart(parentValues chartutil.Values, subchartName string, switch v := parentVals.(type) { case map[string]any: valuesMap = v - case chartutil.Values: + case chartcommon.Values: valuesMap = v } @@ -140,11 +140,11 @@ func scopeValuesForSubchart(parentValues chartutil.Values, subchartName string, return scoped } -func writeInputRenderContext(renderedValues chartutil.Values, chart *helmchart.Chart, renderDir string) error { +func writeInputRenderContext(renderedValues chartcommon.Values, chart *helmchart.Chart, renderDir string) error { renderContext := renderedValues.AsMap() if valuesInterface, ok := renderContext["Values"]; ok { - if chartValues, ok := valuesInterface.(chartutil.Values); ok { + if chartValues, ok := valuesInterface.(chartcommon.Values); ok { renderContext["Values"] = chartValues.AsMap() } } diff --git a/pkg/util/manifest.go b/pkg/util/manifest.go new file mode 100644 index 00000000..748f3682 --- /dev/null +++ b/pkg/util/manifest.go @@ -0,0 +1,46 @@ +package util + +import ( + "regexp" + "strings" +) + +var yamlDocSeparator = regexp.MustCompile(`(?m)^---\s*`) + +// SplitManifests splits a multi-document YAML string into individual manifest +// strings, filtering out empty documents and documents containing only comments. +// Documents are returned in the order they appear in the input. +func SplitManifests(bigFile string) []string { + bigFileTmp := strings.TrimSpace(bigFile) + if bigFileTmp == "" { + return nil + } + + docs := yamlDocSeparator.Split(bigFileTmp, -1) + + var result []string + for _, d := range docs { + d = strings.TrimSpace(d) + if d == "" { + continue + } + + hasContent := false + for _, line := range strings.Split(d, "\n") { + trimmedLine := strings.TrimSpace(line) + if trimmedLine != "" && !strings.HasPrefix(trimmedLine, "#") { + hasContent = true + break + } + } + + if !hasContent { + continue + } + + d += "\n" + result = append(result, d) + } + + return result +} diff --git a/scripts/builder/Dockerfile b/scripts/builder/Dockerfile index a1e101ed..3f39d723 100644 --- a/scripts/builder/Dockerfile +++ b/scripts/builder/Dockerfile @@ -13,7 +13,6 @@ RUN apt-get -y update && \ ADD cmd /.nelm-deps/cmd ADD pkg /.nelm-deps/pkg -ADD internal /.nelm-deps/internal COPY go.mod go.sum Taskfile.dist.yaml /.nelm-deps/ ADD scripts /.nelm-deps/scripts