From c547d1f2ae1cf453debcae88ba70a8163cbe3800 Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Mon, 7 Jul 2025 03:56:07 +0100 Subject: [PATCH 01/21] add color output functionality and tests for release statuses Signed-off-by: Mohammadreza Asadollahifard --- pkg/cli/environment.go | 4 + pkg/cli/output/color.go | 70 +++++++++++ pkg/cli/output/color_test.go | 219 +++++++++++++++++++++++++++++++++++ pkg/cmd/get_all.go | 1 + pkg/cmd/install.go | 5 +- pkg/cmd/list.go | 57 +++++++-- pkg/cmd/release_testing.go | 1 + pkg/cmd/status.go | 8 +- pkg/cmd/upgrade.go | 2 + 9 files changed, 350 insertions(+), 17 deletions(-) create mode 100644 pkg/cli/output/color.go create mode 100644 pkg/cli/output/color_test.go diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 3f2dc00b2..113eef243 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -89,6 +89,8 @@ type EnvSettings struct { BurstLimit int // QPS is queries per second which may be used to avoid throttling. QPS float32 + // NoColor disables colorized output + NoColor bool } func New() *EnvSettings { @@ -109,6 +111,7 @@ func New() *EnvSettings { RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), QPS: envFloat32Or("HELM_QPS", defaultQPS), + NoColor: envBoolOr("NO_COLOR", false), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -160,6 +163,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the directory containing cached repository indexes") 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.BoolVar(&s.NoColor, "no-color", s.NoColor, "disable colorized output") } func envOr(name, def string) string { diff --git a/pkg/cli/output/color.go b/pkg/cli/output/color.go new file mode 100644 index 000000000..9d20f770d --- /dev/null +++ b/pkg/cli/output/color.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 output + +import ( + "os" + + "github.com/fatih/color" + "golang.org/x/term" + + release "helm.sh/helm/v4/pkg/release/v1" +) + +// ColorizeStatus returns a colorized version of the status string based on the status value +func ColorizeStatus(status release.Status, noColor bool) string { + // Disable color if requested or if not in a terminal + if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) { + return status.String() + } + + switch status { + case release.StatusDeployed: + return color.GreenString(status.String()) + case release.StatusFailed: + return color.RedString(status.String()) + case release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback, release.StatusUninstalling: + return color.YellowString(status.String()) + case release.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 or if not in a terminal + if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) { + 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 or if not in a terminal + if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) { + return namespace + } + + // Use cyan for namespaces + return color.CyanString(namespace) +} diff --git a/pkg/cli/output/color_test.go b/pkg/cli/output/color_test.go new file mode 100644 index 000000000..7e8ddddf0 --- /dev/null +++ b/pkg/cli/output/color_test.go @@ -0,0 +1,219 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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 ( + "os" + "strings" + "testing" + + release "helm.sh/helm/v4/pkg/release/v1" +) + +func TestColorizeStatus(t *testing.T) { + // Save original NO_COLOR env var + originalNoColor := os.Getenv("NO_COLOR") + defer func() { + if err := os.Setenv("NO_COLOR", originalNoColor); err != nil { + t.Errorf("Failed to restore NO_COLOR env var: %v", err) + } + }() + + tests := []struct { + name string + status release.Status + noColor bool + envNoColor string + wantColor bool // whether we expect color codes in output + }{ + { + name: "deployed status with color", + status: release.StatusDeployed, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "deployed status without color flag", + status: release.StatusDeployed, + noColor: true, + envNoColor: "", + wantColor: false, + }, + { + name: "deployed status with NO_COLOR env", + status: release.StatusDeployed, + noColor: false, + envNoColor: "1", + wantColor: false, + }, + { + name: "failed status with color", + status: release.StatusFailed, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "pending install status with color", + status: release.StatusPendingInstall, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "unknown status with color", + status: release.StatusUnknown, + noColor: false, + envNoColor: "", + wantColor: true, + }, + { + name: "superseded status with color", + status: release.StatusSuperseded, + noColor: false, + envNoColor: "", + wantColor: false, // superseded doesn't get colored + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil { + t.Fatalf("Failed to set NO_COLOR env var: %v", err) + } + + 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) { + // Save original NO_COLOR env var + originalNoColor := os.Getenv("NO_COLOR") + defer func() { + if err := os.Setenv("NO_COLOR", originalNoColor); err != nil { + t.Errorf("Failed to restore NO_COLOR env var: %v", err) + } + }() + + 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: "", + }, + { + name: "header with NO_COLOR env", + header: "NAME", + noColor: false, + envNoColor: "1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil { + t.Fatalf("Failed to set NO_COLOR env var: %v", err) + } + + 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) { + // Save original NO_COLOR env var + originalNoColor := os.Getenv("NO_COLOR") + defer func() { + if err := os.Setenv("NO_COLOR", originalNoColor); err != nil { + t.Errorf("Failed to restore NO_COLOR env var: %v", err) + } + }() + + 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: "", + }, + { + name: "namespace with NO_COLOR env", + namespace: "default", + noColor: false, + envNoColor: "1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil { + t.Fatalf("Failed to set NO_COLOR env var: %v", err) + } + + 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/cmd/get_all.go b/pkg/cmd/get_all.go index aee92df51..9ada32318 100644 --- a/pkg/cmd/get_all.go +++ b/pkg/cmd/get_all.go @@ -63,6 +63,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: true, showMetadata: true, hideNotes: false, + noColor: settings.NoColor, }) }, } diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 3496a4bbd..78f62aa2e 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -168,6 +168,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: settings.Debug, showMetadata: false, hideNotes: client.HideNotes, + noColor: settings.NoColor, }) }, } @@ -237,13 +238,13 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options client.Version = ">0.0.0-0" } - name, chart, err := client.NameAndChart(args) + name, chartRef, err := client.NameAndChart(args) if err != nil { return nil, err } client.ReleaseName = name - cp, err := client.LocateChart(chart, settings) + cp, err := client.LocateChart(chartRef, settings) if err != nil { return nil, err } diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 5af43adad..a1f31459f 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -106,7 +106,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } - return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders)) + return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders, settings.NoColor)) }, } @@ -146,9 +146,10 @@ type releaseElement struct { type releaseListWriter struct { releases []releaseElement noHeaders bool + noColor bool } -func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool) *releaseListWriter { +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 { @@ -173,26 +174,58 @@ func newReleaseListWriter(releases []*release.Release, timeFormat string, noHead elements = append(elements, element) } - return &releaseListWriter{elements, noHeaders} + return &releaseListWriter{elements, noHeaders, noColor} } -func (r *releaseListWriter) WriteTable(out io.Writer) error { +func (w *releaseListWriter) WriteTable(out io.Writer) error { table := uitable.New() - if !r.noHeaders { - table.AddRow("NAME", "NAMESPACE", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION") + if !w.noHeaders { + table.AddRow( + output.ColorizeHeader("NAME", w.noColor), + output.ColorizeHeader("NAMESPACE", w.noColor), + output.ColorizeHeader("REVISION", w.noColor), + output.ColorizeHeader("UPDATED", w.noColor), + output.ColorizeHeader("STATUS", w.noColor), + output.ColorizeHeader("CHART", w.noColor), + output.ColorizeHeader("APP VERSION", w.noColor), + ) } - for _, r := range r.releases { - table.AddRow(r.Name, r.Namespace, r.Revision, r.Updated, r.Status, r.Chart, r.AppVersion) + for _, r := range w.releases { + // Parse the status string back to a release.Status to use color + var status release.Status + switch r.Status { + case "deployed": + status = release.StatusDeployed + case "failed": + status = release.StatusFailed + case "pending-install": + status = release.StatusPendingInstall + case "pending-upgrade": + status = release.StatusPendingUpgrade + case "pending-rollback": + status = release.StatusPendingRollback + case "uninstalling": + status = release.StatusUninstalling + case "uninstalled": + status = release.StatusUninstalled + case "superseded": + status = release.StatusSuperseded + case "unknown": + status = release.StatusUnknown + default: + status = release.Status(r.Status) + } + table.AddRow(r.Name, output.ColorizeNamespace(r.Namespace, w.noColor), r.Revision, r.Updated, output.ColorizeStatus(status, w.noColor), r.Chart, r.AppVersion) } return output.EncodeTable(out, table) } -func (r *releaseListWriter) WriteJSON(out io.Writer) error { - return output.EncodeJSON(out, r.releases) +func (w *releaseListWriter) WriteJSON(out io.Writer) error { + return output.EncodeJSON(out, w.releases) } -func (r *releaseListWriter) WriteYAML(out io.Writer) error { - return output.EncodeYAML(out, r.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' diff --git a/pkg/cmd/release_testing.go b/pkg/cmd/release_testing.go index 1dac28534..e43c58145 100644 --- a/pkg/cmd/release_testing.go +++ b/pkg/cmd/release_testing.go @@ -78,6 +78,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command debug: settings.Debug, showMetadata: false, hideNotes: client.HideNotes, + noColor: settings.NoColor, }); err != nil { return err } diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 2b1138786..c2960f823 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -84,6 +84,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: false, showMetadata: false, hideNotes: false, + noColor: settings.NoColor, }) }, } @@ -112,6 +113,7 @@ type statusPrinter struct { debug bool showMetadata bool hideNotes bool + noColor bool } func (s statusPrinter) WriteJSON(out io.Writer) error { @@ -130,8 +132,8 @@ func (s statusPrinter) WriteTable(out io.Writer) error { if !s.release.Info.LastDeployed.IsZero() { _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) } - _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", s.release.Namespace) - _, _ = fmt.Fprintf(out, "STATUS: %s\n", s.release.Info.Status.String()) + _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", output.ColorizeNamespace(s.release.Namespace, s.noColor)) + _, _ = fmt.Fprintf(out, "STATUS: %s\n", output.ColorizeStatus(s.release.Info.Status, s.noColor)) _, _ = fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) if s.showMetadata { _, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) @@ -218,7 +220,7 @@ func (s statusPrinter) WriteTable(out io.Writer) error { // Hide notes from output - option in install and upgrades if !s.hideNotes && len(s.release.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) + _, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(s.release.Info.Notes)) } return nil } diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index d4e7b4852..32d4f230b 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -166,6 +166,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: settings.Debug, showMetadata: false, hideNotes: instClient.HideNotes, + noColor: settings.NoColor, }) } else if err != nil { return err @@ -257,6 +258,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: settings.Debug, showMetadata: false, hideNotes: client.HideNotes, + noColor: settings.NoColor, }) }, } From 1e00790b8c9692ab95b6c64133ed0f5d8f053930 Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Mon, 7 Jul 2025 23:04:17 +0100 Subject: [PATCH 02/21] refactor tests to use t.Setenv for NO_COLOR environment variable Signed-off-by: Mohammadreza Asadollahifard --- pkg/cli/output/color_test.go | 34 +++------------------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/pkg/cli/output/color_test.go b/pkg/cli/output/color_test.go index 7e8ddddf0..c84e2c359 100644 --- a/pkg/cli/output/color_test.go +++ b/pkg/cli/output/color_test.go @@ -17,7 +17,6 @@ limitations under the License. package output import ( - "os" "strings" "testing" @@ -25,13 +24,6 @@ import ( ) func TestColorizeStatus(t *testing.T) { - // Save original NO_COLOR env var - originalNoColor := os.Getenv("NO_COLOR") - defer func() { - if err := os.Setenv("NO_COLOR", originalNoColor); err != nil { - t.Errorf("Failed to restore NO_COLOR env var: %v", err) - } - }() tests := []struct { name string @@ -93,9 +85,7 @@ func TestColorizeStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil { - t.Fatalf("Failed to set NO_COLOR env var: %v", err) - } + t.Setenv("NO_COLOR", tt.envNoColor) result := ColorizeStatus(tt.status, tt.noColor) @@ -117,13 +107,6 @@ func TestColorizeStatus(t *testing.T) { } func TestColorizeHeader(t *testing.T) { - // Save original NO_COLOR env var - originalNoColor := os.Getenv("NO_COLOR") - defer func() { - if err := os.Setenv("NO_COLOR", originalNoColor); err != nil { - t.Errorf("Failed to restore NO_COLOR env var: %v", err) - } - }() tests := []struct { name string @@ -153,9 +136,7 @@ func TestColorizeHeader(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil { - t.Fatalf("Failed to set NO_COLOR env var: %v", err) - } + t.Setenv("NO_COLOR", tt.envNoColor) result := ColorizeHeader(tt.header, tt.noColor) @@ -168,13 +149,6 @@ func TestColorizeHeader(t *testing.T) { } func TestColorizeNamespace(t *testing.T) { - // Save original NO_COLOR env var - originalNoColor := os.Getenv("NO_COLOR") - defer func() { - if err := os.Setenv("NO_COLOR", originalNoColor); err != nil { - t.Errorf("Failed to restore NO_COLOR env var: %v", err) - } - }() tests := []struct { name string @@ -204,9 +178,7 @@ func TestColorizeNamespace(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := os.Setenv("NO_COLOR", tt.envNoColor); err != nil { - t.Fatalf("Failed to set NO_COLOR env var: %v", err) - } + t.Setenv("NO_COLOR", tt.envNoColor) result := ColorizeNamespace(tt.namespace, tt.noColor) From 96c54a2963ad71bc6627fbe3f7992ab1dd418cbc Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Fri, 11 Jul 2025 20:27:05 +0100 Subject: [PATCH 03/21] refactor color output functions to simplify noColor checks Signed-off-by: Mohammadreza Asadollahifard --- pkg/cli/output/color.go | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pkg/cli/output/color.go b/pkg/cli/output/color.go index 9d20f770d..93bbbe56e 100644 --- a/pkg/cli/output/color.go +++ b/pkg/cli/output/color.go @@ -17,18 +17,15 @@ limitations under the License. package output import ( - "os" - "github.com/fatih/color" - "golang.org/x/term" release "helm.sh/helm/v4/pkg/release/v1" ) // ColorizeStatus returns a colorized version of the status string based on the status value func ColorizeStatus(status release.Status, noColor bool) string { - // Disable color if requested or if not in a terminal - if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) { + // Disable color if requested + if noColor { return status.String() } @@ -49,8 +46,8 @@ func ColorizeStatus(status release.Status, noColor bool) string { // ColorizeHeader returns a colorized version of a header string func ColorizeHeader(header string, noColor bool) string { - // Disable color if requested or if not in a terminal - if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) { + // Disable color if requested + if noColor { return header } @@ -60,8 +57,8 @@ func ColorizeHeader(header string, noColor bool) string { // ColorizeNamespace returns a colorized version of a namespace string func ColorizeNamespace(namespace string, noColor bool) string { - // Disable color if requested or if not in a terminal - if noColor || os.Getenv("NO_COLOR") != "" || !term.IsTerminal(int(os.Stdout.Fd())) { + // Disable color if requested + if noColor { return namespace } From b72db06c4925e1f4e0a986aa44da2b643117dacf Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Fri, 11 Jul 2025 20:52:40 +0100 Subject: [PATCH 04/21] refactor: replace NoColor with ColorMode for improved color output control Signed-off-by: Mohammadreza Asadollahifard --- pkg/cli/environment.go | 48 ++++++++++++++++++++++++++++++++++---- pkg/cmd/get_all.go | 2 +- pkg/cmd/install.go | 2 +- pkg/cmd/list.go | 2 +- pkg/cmd/release_testing.go | 2 +- pkg/cmd/root.go | 38 ++++++++++++++++++++++++++++++ pkg/cmd/status.go | 2 +- pkg/cmd/upgrade.go | 4 ++-- 8 files changed, 89 insertions(+), 11 deletions(-) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 113eef243..223a7cb15 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -89,8 +89,8 @@ type EnvSettings struct { BurstLimit int // QPS is queries per second which may be used to avoid throttling. QPS float32 - // NoColor disables colorized output - NoColor bool + // ColorMode controls colorized output (never, auto, always) + ColorMode string } func New() *EnvSettings { @@ -111,7 +111,7 @@ func New() *EnvSettings { RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), QPS: envFloat32Or("HELM_QPS", defaultQPS), - NoColor: envBoolOr("NO_COLOR", false), + ColorMode: envColorMode(), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -163,7 +163,8 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the directory containing cached repository indexes") 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.BoolVar(&s.NoColor, "no-color", s.NoColor, "disable colorized output") + 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 { @@ -217,6 +218,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], @@ -269,3 +287,25 @@ func (s *EnvSettings) SetNamespace(namespace string) { func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter { return s.config } + +// ColorEnabled returns true if color output should be enabled based on the ColorMode setting +func (s *EnvSettings) ColorEnabled() bool { + switch s.ColorMode { + case "never": + return false + case "always": + return true + case "auto": + // Auto mode is handled by fatih/color's built-in terminal detection + // We just need to not override it + return true + default: + return true + } +} + +// ShouldDisableColor returns true if color output should be disabled +// This is the inverse of ColorEnabled for backward compatibility with noColor parameters +func (s *EnvSettings) ShouldDisableColor() bool { + return s.ColorMode == "never" +} diff --git a/pkg/cmd/get_all.go b/pkg/cmd/get_all.go index 9ada32318..32744796c 100644 --- a/pkg/cmd/get_all.go +++ b/pkg/cmd/get_all.go @@ -63,7 +63,7 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: true, showMetadata: true, hideNotes: false, - noColor: settings.NoColor, + noColor: settings.ShouldDisableColor(), }) }, } diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 78f62aa2e..f1a7b18c8 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -168,7 +168,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: settings.Debug, showMetadata: false, hideNotes: client.HideNotes, - noColor: settings.NoColor, + noColor: settings.ShouldDisableColor(), }) }, } diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index a1f31459f..016d7663a 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -106,7 +106,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } - return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders, settings.NoColor)) + return outfmt.Write(out, newReleaseListWriter(results, client.TimeFormat, client.NoHeaders, settings.ShouldDisableColor())) }, } diff --git a/pkg/cmd/release_testing.go b/pkg/cmd/release_testing.go index e43c58145..b43b67ca0 100644 --- a/pkg/cmd/release_testing.go +++ b/pkg/cmd/release_testing.go @@ -78,7 +78,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command debug: settings.Debug, showMetadata: false, hideNotes: client.HideNotes, - noColor: settings.NoColor, + noColor: settings.ShouldDisableColor(), }); err != nil { return err } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 4eb5da494..8451821b6 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -26,6 +26,7 @@ import ( "os" "strings" + "github.com/fatih/color" "github.com/spf13/cobra" "sigs.k8s.io/yaml" @@ -80,6 +81,8 @@ Environment variables: | $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: auto) | +| $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: @@ -129,6 +132,20 @@ func SetupLogging(debug bool) { 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", @@ -160,6 +177,27 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg flags.Parse(args) logSetup(settings.Debug) + + // 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) { diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index c2960f823..2177df922 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -84,7 +84,7 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: false, showMetadata: false, hideNotes: false, - noColor: settings.NoColor, + noColor: settings.ShouldDisableColor(), }) }, } diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index 32d4f230b..50e18299d 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -166,7 +166,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: settings.Debug, showMetadata: false, hideNotes: instClient.HideNotes, - noColor: settings.NoColor, + noColor: settings.ShouldDisableColor(), }) } else if err != nil { return err @@ -258,7 +258,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { debug: settings.Debug, showMetadata: false, hideNotes: client.HideNotes, - noColor: settings.NoColor, + noColor: settings.ShouldDisableColor(), }) }, } From ba8f70ae0b63198868495f510768747b25ddf042 Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Fri, 11 Jul 2025 21:04:27 +0100 Subject: [PATCH 05/21] refactor: move color package to internal/cli/output Signed-off-by: Mohammadreza Asadollahifard --- {pkg => internal}/cli/output/color.go | 0 {pkg => internal}/cli/output/color_test.go | 0 pkg/cmd/list.go | 17 +++++++++-------- pkg/cmd/status.go | 5 +++-- 4 files changed, 12 insertions(+), 10 deletions(-) rename {pkg => internal}/cli/output/color.go (100%) rename {pkg => internal}/cli/output/color_test.go (100%) diff --git a/pkg/cli/output/color.go b/internal/cli/output/color.go similarity index 100% rename from pkg/cli/output/color.go rename to internal/cli/output/color.go diff --git a/pkg/cli/output/color_test.go b/internal/cli/output/color_test.go similarity index 100% rename from pkg/cli/output/color_test.go rename to internal/cli/output/color_test.go diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 016d7663a..a0041d16c 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -28,6 +28,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/cli/output" + coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -181,13 +182,13 @@ func (w *releaseListWriter) WriteTable(out io.Writer) error { table := uitable.New() if !w.noHeaders { table.AddRow( - output.ColorizeHeader("NAME", w.noColor), - output.ColorizeHeader("NAMESPACE", w.noColor), - output.ColorizeHeader("REVISION", w.noColor), - output.ColorizeHeader("UPDATED", w.noColor), - output.ColorizeHeader("STATUS", w.noColor), - output.ColorizeHeader("CHART", w.noColor), - output.ColorizeHeader("APP VERSION", w.noColor), + 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 { @@ -215,7 +216,7 @@ func (w *releaseListWriter) WriteTable(out io.Writer) error { default: status = release.Status(r.Status) } - table.AddRow(r.Name, output.ColorizeNamespace(r.Namespace, w.noColor), r.Revision, r.Updated, output.ColorizeStatus(status, w.noColor), r.Chart, r.AppVersion) + 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) } diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 2177df922..3198d468f 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -31,6 +31,7 @@ import ( "helm.sh/helm/v4/pkg/action" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/cli/output" + coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -132,8 +133,8 @@ func (s statusPrinter) WriteTable(out io.Writer) error { if !s.release.Info.LastDeployed.IsZero() { _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) } - _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", output.ColorizeNamespace(s.release.Namespace, s.noColor)) - _, _ = fmt.Fprintf(out, "STATUS: %s\n", output.ColorizeStatus(s.release.Info.Status, s.noColor)) + _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(s.release.Namespace, s.noColor)) + _, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(s.release.Info.Status, s.noColor)) _, _ = fmt.Fprintf(out, "REVISION: %d\n", s.release.Version) if s.showMetadata { _, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) From d28343550ffecab72bbf35563312934f27f79231 Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Fri, 11 Jul 2025 21:08:32 +0100 Subject: [PATCH 06/21] feat: make color output opt-in by default Signed-off-by: Mohammadreza Asadollahifard --- pkg/cli/environment.go | 4 ++-- pkg/cmd/root.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 223a7cb15..111338e8a 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -231,8 +231,8 @@ func envColorMode() string { return v } } - // Default to auto - return "auto" + // Default to never (disabled) until more commands support color + return "never" } func (s *EnvSettings) EnvVars() map[string]string { diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 8451821b6..3d0180d86 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -81,7 +81,7 @@ Environment variables: | $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: auto) | +| $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: From c1b3a835141ae404f3ecafd856ba63c307d18688 Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Fri, 11 Jul 2025 21:19:16 +0100 Subject: [PATCH 07/21] refactor: clean up color output imports in list, root, and status files Signed-off-by: Mohammadreza Asadollahifard --- pkg/cmd/list.go | 2 +- pkg/cmd/root.go | 6 +++--- pkg/cmd/status.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index a0041d16c..55d828036 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -26,9 +26,9 @@ import ( "github.com/gosuri/uitable" "github.com/spf13/cobra" + coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/cli/output" - coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" ) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 3d0180d86..f43ce7abe 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -177,7 +177,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg flags.Parse(args) logSetup(settings.Debug) - + // Validate color mode setting switch settings.ColorMode { case "never", "auto", "always": @@ -185,7 +185,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg 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) @@ -193,7 +193,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg _ = 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 diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 3198d468f..aa836f9f3 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -28,10 +28,10 @@ import ( "k8s.io/kubectl/pkg/cmd/get" + coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/action" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/cli/output" - coloroutput "helm.sh/helm/v4/internal/cli/output" "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" ) From 74f2805f01de5e5dd5f449fcccb81b7a391cf641 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Sun, 15 Jun 2025 11:31:48 -0700 Subject: [PATCH 08/21] Rename 'force' to 'force-replace' Signed-off-by: George Jenkins --- pkg/action/install.go | 13 ++++++++----- pkg/action/rollback.go | 19 +++++++++++-------- pkg/action/upgrade.go | 8 ++++---- pkg/action/validate.go | 6 +++--- pkg/cmd/install.go | 4 +++- pkg/cmd/rollback.go | 4 +++- pkg/cmd/upgrade.go | 6 ++++-- 7 files changed, 36 insertions(+), 24 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 440f41baa..2904965de 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -71,8 +71,11 @@ type Install struct { ChartPathOptions - ClientOnly bool - Force bool + ClientOnly bool + // ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway. + // + // This should be used with caution. + ForceReplace bool CreateNamespace bool DryRun bool DryRunOption string @@ -346,7 +349,7 @@ func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals ma return nil, fmt.Errorf("unable to build kubernetes objects from release manifest: %w", err) } - // It is safe to use "force" here because these are resources currently rendered by the chart. + // It is safe to use "forceOwnership" here because these are resources currently rendered by the chart. err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true)) if err != nil { return nil, err @@ -468,9 +471,9 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource _, err = i.cfg.KubeClient.Create(resources) } else if len(resources) > 0 { if i.TakeOwnership { - _, err = i.cfg.KubeClient.(kube.InterfaceThreeWayMerge).UpdateThreeWayMerge(toBeAdopted, resources, i.Force) + _, err = i.cfg.KubeClient.(kube.InterfaceThreeWayMerge).UpdateThreeWayMerge(toBeAdopted, resources, i.ForceReplace) } else { - _, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.Force) + _, err = i.cfg.KubeClient.Update(toBeAdopted, resources, i.ForceReplace) } } if err != nil { diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index 1dc0c7f84..f529fa422 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -35,13 +35,16 @@ import ( type Rollback struct { cfg *Configuration - Version int - Timeout time.Duration - WaitStrategy kube.WaitStrategy - WaitForJobs bool - DisableHooks bool - DryRun bool - Force bool // will (if true) force resource upgrade through uninstall/recreate if needed + Version int + Timeout time.Duration + WaitStrategy kube.WaitStrategy + WaitForJobs bool + DisableHooks bool + DryRun bool + // ForceReplace will, if set to `true`, ignore certain warnings and perform the rollback anyway. + // + // This should be used with caution. + ForceReplace bool CleanupOnFail bool MaxHistory int // MaxHistory limits the maximum number of revisions saved per release } @@ -187,7 +190,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas if err != nil { return targetRelease, fmt.Errorf("unable to set metadata visitor from target release: %w", err) } - results, err := r.cfg.KubeClient.Update(current, target, r.Force) + results, err := r.cfg.KubeClient.Update(current, target, r.ForceReplace) if err != nil { msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 271bc8aa9..0567c8de2 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -77,10 +77,10 @@ type Upgrade struct { // HideSecret can be set to true when DryRun is enabled in order to hide // Kubernetes Secrets in the output. It cannot be used outside of DryRun. HideSecret bool - // Force will, if set to `true`, ignore certain warnings and perform the upgrade anyway. + // ForceReplace will, if set to `true`, ignore certain warnings and perform the upgrade anyway. // // This should be used with caution. - Force bool + ForceReplace bool // ResetValues will reset the values to the chart's built-ins rather than merging with existing. ResetValues bool // ReuseValues will reuse the user's last supplied values. @@ -426,7 +426,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name) } - results, err := u.cfg.KubeClient.Update(current, target, u.Force) + results, err := u.cfg.KubeClient.Update(current, target, u.ForceReplace) if err != nil { u.cfg.recordRelease(originalRelease) u.reportToPerformUpgrade(c, upgradedRelease, results.Created, err) @@ -525,7 +525,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e } rollin.WaitForJobs = u.WaitForJobs rollin.DisableHooks = u.DisableHooks - rollin.Force = u.Force + rollin.ForceReplace = u.ForceReplace rollin.Timeout = u.Timeout if rollErr := rollin.Run(rel.Name); rollErr != nil { return rel, fmt.Errorf("an error occurred while rolling back the release. original upgrade error: %w: %w", err, rollErr) diff --git a/pkg/action/validate.go b/pkg/action/validate.go index e1021860f..761ccba47 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -130,16 +130,16 @@ func requireValue(meta map[string]string, k, v string) error { return nil } -// setMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing +// setMetadataVisitor adds release tracking metadata to all resources. If forceOwnership 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 { +func setMetadataVisitor(releaseName, releaseNamespace string, forceOwnership bool) resource.VisitorFunc { return func(info *resource.Info, err error) error { if err != nil { return err } - if !force { + if !forceOwnership { if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil { return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err) } diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 3496a4bbd..7fca8585a 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -192,7 +192,9 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal // The true/false part is meant to reflect some legacy behavior while none is equal to "". f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.") f.Lookup("dry-run").NoOptDefVal = "client" - f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") + f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") + f.BoolVar(&client.ForceReplace, "force", false, "deprecated") + f.MarkDeprecated("force", "use --force-replace instead") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") f.BoolVar(&client.Replace, "replace", false, "reuse the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") diff --git a/pkg/cmd/rollback.go b/pkg/cmd/rollback.go index 6658d3fd6..4b7f3016d 100644 --- a/pkg/cmd/rollback.go +++ b/pkg/cmd/rollback.go @@ -77,7 +77,9 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback") - f.BoolVar(&client.Force, "force", false, "force resource update through delete/recreate if needed") + f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") + f.BoolVar(&client.ForceReplace, "force", false, "deprecated") + f.MarkDeprecated("force", "use --force-replace instead") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during rollback") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&client.WaitForJobs, "wait-for-jobs", false, "if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout") diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index d4e7b4852..ced4bb526 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -130,7 +130,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient := action.NewInstall(cfg) instClient.CreateNamespace = createNamespace instClient.ChartPathOptions = client.ChartPathOptions - instClient.Force = client.Force + instClient.ForceReplace = client.ForceReplace instClient.DryRun = client.DryRun instClient.DryRunOption = client.DryRunOption instClient.DisableHooks = client.DisableHooks @@ -268,7 +268,9 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVar(&client.DryRunOption, "dry-run", "", "simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections.") f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") f.Lookup("dry-run").NoOptDefVal = "client" - f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") + f.BoolVar(&client.ForceReplace, "force-replace", false, "force resource updates by replacement") + f.BoolVar(&client.ForceReplace, "force", false, "deprecated") + f.MarkDeprecated("force", "use --force-replace instead") f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks") f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema") f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled") From cf06c6d418c109dcbfd23e4e9ebda2c3655210b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C5=A1per=20Grom?= Date: Sat, 19 Jul 2025 08:17:54 +0200 Subject: [PATCH 09/21] fix: LFX health score badge link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Gašper Grom --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ef994e742..66fdab041 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v4) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/helm/helm/badge)](https://scorecard.dev/viewer/?uri=github.com/helm/helm) -[![LFX Health Score](https://img.shields.io/static/v1?label=Health%20Score&message=Healthy&color=A7F3D0&logo=linuxfoundation&logoColor=white&style=flat)](https://insights.linuxfoundation.org/project/helm) +[![LFX Health Score](https://insights.production.lfx.dev/api/badge/health-score?project=helm)](https://insights.linuxfoundation.org/project/helm) Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources. From 9f6beaad48cd74218c7bef20522a500873d90d2c Mon Sep 17 00:00:00 2001 From: Borys Hulii Date: Mon, 21 Jul 2025 09:36:57 +0200 Subject: [PATCH 10/21] fix: k8s version parsing to match original Signed-off-by: Borys Hulii --- pkg/chart/v2/util/capabilities.go | 10 ++++++---- pkg/chart/v2/util/capabilities_test.go | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/pkg/chart/v2/util/capabilities.go b/pkg/chart/v2/util/capabilities.go index 23b6d46fa..19d62c5e3 100644 --- a/pkg/chart/v2/util/capabilities.go +++ b/pkg/chart/v2/util/capabilities.go @@ -20,11 +20,11 @@ import ( "slices" "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" + k8sversion "k8s.io/apimachinery/pkg/util/version" helmversion "helm.sh/helm/v4/internal/version" ) @@ -85,14 +85,16 @@ 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) + // 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 } return &KubeVersion{ Version: "v" + sv.String(), - Major: strconv.FormatUint(sv.Major(), 10), - Minor: strconv.FormatUint(sv.Minor(), 10), + Major: strconv.FormatUint(uint64(sv.Major()), 10), + Minor: strconv.FormatUint(uint64(sv.Minor()), 10), }, nil } diff --git a/pkg/chart/v2/util/capabilities_test.go b/pkg/chart/v2/util/capabilities_test.go index aa9be9db8..e5513b3fd 100644 --- a/pkg/chart/v2/util/capabilities_test.go +++ b/pkg/chart/v2/util/capabilities_test.go @@ -82,3 +82,19 @@ func TestParseKubeVersion(t *testing.T) { t.Errorf("Expected parsed KubeVersion.Minor to be 16, got %q", kv.Minor) } } + +func TestParseKubeVersionSuffix(t *testing.T) { + kv, err := ParseKubeVersion("v1.28+") + if err != nil { + t.Errorf("Expected v1.28+ to parse successfully") + } + if kv.Version != "v1.28" { + t.Errorf("Expected parsed KubeVersion.Version to be v1.28, got %q", kv.String()) + } + if kv.Major != "1" { + t.Errorf("Expected parsed KubeVersion.Major to be 1, got %q", kv.Major) + } + if kv.Minor != "28" { + t.Errorf("Expected parsed KubeVersion.Minor to be 28, got %q", kv.Minor) + } +} From f3065ff1ba131a84bee61ef54e4d5c81a2ed3763 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Mon, 21 Jul 2025 18:03:16 -0700 Subject: [PATCH 11/21] Remove plugin deprecated 'UseTunnelDeprecated' Signed-off-by: George Jenkins --- pkg/plugin/plugin.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go index 9d79ab4fc..930bf3664 100644 --- a/pkg/plugin/plugin.go +++ b/pkg/plugin/plugin.go @@ -132,12 +132,6 @@ type Metadata struct { // 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. From d46857fb3e5ab120733231b5833b5357eadd4799 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:52:43 +0000 Subject: [PATCH 12/21] chore(deps): bump sigs.k8s.io/kustomize/kyaml from 0.20.0 to 0.20.1 Bumps [sigs.k8s.io/kustomize/kyaml](https://github.com/kubernetes-sigs/kustomize) from 0.20.0 to 0.20.1. - [Release notes](https://github.com/kubernetes-sigs/kustomize/releases) - [Commits](https://github.com/kubernetes-sigs/kustomize/compare/api/v0.20.0...api/v0.20.1) --- updated-dependencies: - dependency-name: sigs.k8s.io/kustomize/kyaml dependency-version: 0.20.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ed63d9a53..2b6f3153b 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( k8s.io/kubectl v0.33.3 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.21.0 - sigs.k8s.io/kustomize/kyaml v0.20.0 + sigs.k8s.io/kustomize/kyaml v0.20.1 sigs.k8s.io/yaml v1.5.0 ) diff --git a/go.sum b/go.sum index 4742a54e7..1ae78d67d 100644 --- a/go.sum +++ b/go.sum @@ -537,8 +537,8 @@ sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7np sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.0 h1:xPLqcobHI0bThyRUteO+nCV8G4d1Rlo5HafO57VRcas= sigs.k8s.io/kustomize/api v0.20.0/go.mod h1:F6CfaV27oevRCMJgehLqyX81dlUnRX/Fc13Uo7+OSo4= -sigs.k8s.io/kustomize/kyaml v0.20.0 h1:tT8KMKi4R3hCJ1+9HDdek2VoXpkerP92ZfF6fDgGw14= -sigs.k8s.io/kustomize/kyaml v0.20.0/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= From 0865d703048dfbc2e949e34a681fecb34aacba32 Mon Sep 17 00:00:00 2001 From: Mohammadreza Asadollahifard Date: Wed, 23 Jul 2025 23:05:18 +0100 Subject: [PATCH 13/21] refactor: change default color output setting to auto and remove ColorEnabled method Signed-off-by: Mohammadreza Asadollahifard --- pkg/cli/environment.go | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 111338e8a..c5f87cf24 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -231,8 +231,8 @@ func envColorMode() string { return v } } - // Default to never (disabled) until more commands support color - return "never" + // Default to auto + return "auto" } func (s *EnvSettings) EnvVars() map[string]string { @@ -288,24 +288,7 @@ func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter { return s.config } -// ColorEnabled returns true if color output should be enabled based on the ColorMode setting -func (s *EnvSettings) ColorEnabled() bool { - switch s.ColorMode { - case "never": - return false - case "always": - return true - case "auto": - // Auto mode is handled by fatih/color's built-in terminal detection - // We just need to not override it - return true - default: - return true - } -} - // ShouldDisableColor returns true if color output should be disabled -// This is the inverse of ColorEnabled for backward compatibility with noColor parameters func (s *EnvSettings) ShouldDisableColor() bool { return s.ColorMode == "never" } From 1674fb6797ae3e5cb924f5d47b24112e9cc58274 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 22:11:26 +0000 Subject: [PATCH 14/21] chore(deps): bump sigs.k8s.io/yaml from 1.5.0 to 1.6.0 Bumps [sigs.k8s.io/yaml](https://github.com/kubernetes-sigs/yaml) from 1.5.0 to 1.6.0. - [Release notes](https://github.com/kubernetes-sigs/yaml/releases) - [Changelog](https://github.com/kubernetes-sigs/yaml/blob/master/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/yaml/compare/v1.5.0...v1.6.0) --- updated-dependencies: - dependency-name: sigs.k8s.io/yaml dependency-version: 1.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2b6f3153b..e7978c530 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.21.0 sigs.k8s.io/kustomize/kyaml v0.20.1 - sigs.k8s.io/yaml v1.5.0 + sigs.k8s.io/yaml v1.6.0 ) require ( diff --git a/go.sum b/go.sum index 1ae78d67d..464ad8590 100644 --- a/go.sum +++ b/go.sum @@ -545,5 +545,5 @@ sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxO sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -sigs.k8s.io/yaml v1.5.0 h1:M10b2U7aEUY6hRtU870n2VTPgR5RZiL/I6Lcc2F4NUQ= -sigs.k8s.io/yaml v1.5.0/go.mod h1:wZs27Rbxoai4C0f8/9urLZtZtF3avA3gKvGyPdDqTO4= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= From 70257f5cd646140119bc59467b8600588b42d169 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Tue, 22 Jul 2025 13:00:01 -0400 Subject: [PATCH 15/21] Initial addition of v3 charts This change adds v3 charts. The code for the chart, including a loader, is present. It is based on v2 charts as a starting point. Note, this change does not make the charts available for use with Helm CLI commands or the action package. That will be in follow-up changes. Signed-off-by: Matt Farina --- internal/chart/v3/chart.go | 172 ++++ internal/chart/v3/chart_test.go | 211 +++++ internal/chart/v3/dependency.go | 82 ++ internal/chart/v3/dependency_test.go | 44 + internal/chart/v3/doc.go | 21 + internal/chart/v3/errors.go | 30 + internal/chart/v3/file.go | 27 + internal/chart/v3/fuzz_test.go | 48 + internal/chart/v3/loader/archive.go | 234 +++++ internal/chart/v3/loader/archive_test.go | 92 ++ internal/chart/v3/loader/directory.go | 121 +++ internal/chart/v3/loader/load.go | 219 +++++ internal/chart/v3/loader/load_test.go | 711 +++++++++++++++ internal/chart/v3/loader/testdata/LICENSE | 1 + .../v3/loader/testdata/albatross/Chart.yaml | 4 + .../v3/loader/testdata/albatross/values.yaml | 4 + .../v3/loader/testdata/frobnitz-1.2.3.tgz | Bin 0 -> 3420 bytes .../testdata/frobnitz.v3.reqs/.helmignore | 1 + .../testdata/frobnitz.v3.reqs/Chart.yaml | 27 + .../testdata/frobnitz.v3.reqs/INSTALL.txt | 1 + .../loader/testdata/frobnitz.v3.reqs/LICENSE | 1 + .../testdata/frobnitz.v3.reqs/README.md | 11 + .../frobnitz.v3.reqs/charts/_ignore_me | 1 + .../frobnitz.v3.reqs/charts/alpine/Chart.yaml | 5 + .../frobnitz.v3.reqs/charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../frobnitz.v3.reqs/charts/mariner-4.3.2.tgz | Bin 0 -> 967 bytes .../testdata/frobnitz.v3.reqs/docs/README.md | 1 + .../loader/testdata/frobnitz.v3.reqs/icon.svg | 8 + .../testdata/frobnitz.v3.reqs/ignore/me.txt | 0 .../frobnitz.v3.reqs/templates/template.tpl | 1 + .../testdata/frobnitz.v3.reqs/values.yaml | 6 + .../v3/loader/testdata/frobnitz/.helmignore | 1 + .../v3/loader/testdata/frobnitz/Chart.lock | 8 + .../v3/loader/testdata/frobnitz/Chart.yaml | 27 + .../v3/loader/testdata/frobnitz/INSTALL.txt | 1 + .../chart/v3/loader/testdata/frobnitz/LICENSE | 1 + .../v3/loader/testdata/frobnitz/README.md | 11 + .../testdata/frobnitz/charts/_ignore_me | 1 + .../frobnitz/charts/alpine/Chart.yaml | 5 + .../testdata/frobnitz/charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../frobnitz/charts/alpine/values.yaml | 2 + .../frobnitz/charts/mariner-4.3.2.tgz | Bin 0 -> 910 bytes .../loader/testdata/frobnitz/docs/README.md | 1 + .../v3/loader/testdata/frobnitz/icon.svg | 8 + .../v3/loader/testdata/frobnitz/ignore/me.txt | 0 .../testdata/frobnitz/templates/template.tpl | 1 + .../v3/loader/testdata/frobnitz/values.yaml | 6 + .../testdata/frobnitz_backslash-1.2.3.tgz | Bin 0 -> 3434 bytes .../testdata/frobnitz_backslash/.helmignore | 1 + .../testdata/frobnitz_backslash/Chart.lock | 8 + .../testdata/frobnitz_backslash/Chart.yaml | 27 + .../testdata/frobnitz_backslash/INSTALL.txt | 1 + .../testdata/frobnitz_backslash/LICENSE | 1 + .../testdata/frobnitz_backslash/README.md | 11 + .../frobnitz_backslash/charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 910 bytes .../frobnitz_backslash/docs/README.md | 1 + .../testdata/frobnitz_backslash/icon.svg | 8 + .../testdata/frobnitz_backslash/ignore/me.txt | 0 .../frobnitz_backslash/templates/template.tpl | 1 + .../testdata/frobnitz_backslash/values.yaml | 6 + .../v3/loader/testdata/frobnitz_with_bom.tgz | Bin 0 -> 3453 bytes .../testdata/frobnitz_with_bom/.helmignore | 1 + .../testdata/frobnitz_with_bom/Chart.lock | 8 + .../testdata/frobnitz_with_bom/Chart.yaml | 27 + .../testdata/frobnitz_with_bom/INSTALL.txt | 1 + .../loader/testdata/frobnitz_with_bom/LICENSE | 1 + .../testdata/frobnitz_with_bom/README.md | 11 + .../frobnitz_with_bom/charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../frobnitz_with_bom/charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 910 bytes .../testdata/frobnitz_with_bom/docs/README.md | 1 + .../testdata/frobnitz_with_bom/icon.svg | 8 + .../testdata/frobnitz_with_bom/ignore/me.txt | 0 .../frobnitz_with_bom/templates/template.tpl | 1 + .../testdata/frobnitz_with_bom/values.yaml | 6 + .../frobnitz_with_dev_null/.helmignore | 1 + .../frobnitz_with_dev_null/Chart.lock | 8 + .../frobnitz_with_dev_null/Chart.yaml | 27 + .../frobnitz_with_dev_null/INSTALL.txt | 1 + .../testdata/frobnitz_with_dev_null/LICENSE | 1 + .../testdata/frobnitz_with_dev_null/README.md | 11 + .../frobnitz_with_dev_null/charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 910 bytes .../frobnitz_with_dev_null/docs/README.md | 1 + .../testdata/frobnitz_with_dev_null/icon.svg | 8 + .../frobnitz_with_dev_null/ignore/me.txt | 0 .../testdata/frobnitz_with_dev_null/null | 1 + .../templates/template.tpl | 1 + .../frobnitz_with_dev_null/values.yaml | 6 + .../frobnitz_with_symlink/.helmignore | 1 + .../testdata/frobnitz_with_symlink/Chart.lock | 8 + .../testdata/frobnitz_with_symlink/Chart.yaml | 27 + .../frobnitz_with_symlink/INSTALL.txt | 1 + .../testdata/frobnitz_with_symlink/README.md | 11 + .../frobnitz_with_symlink/charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 910 bytes .../frobnitz_with_symlink/docs/README.md | 1 + .../testdata/frobnitz_with_symlink/icon.svg | 8 + .../frobnitz_with_symlink/ignore/me.txt | 0 .../templates/template.tpl | 1 + .../frobnitz_with_symlink/values.yaml | 6 + internal/chart/v3/loader/testdata/genfrob.sh | 18 + .../v3/loader/testdata/mariner/Chart.yaml | 9 + .../mariner/charts/albatross-0.1.0.tgz | Bin 0 -> 282 bytes .../mariner/templates/placeholder.tpl | 1 + .../v3/loader/testdata/mariner/values.yaml | 7 + internal/chart/v3/metadata.go | 178 ++++ internal/chart/v3/metadata_test.go | 201 +++++ internal/chart/v3/util/capabilities.go | 122 +++ internal/chart/v3/util/capabilities_test.go | 84 ++ internal/chart/v3/util/chartfile.go | 96 ++ internal/chart/v3/util/chartfile_test.go | 117 +++ internal/chart/v3/util/coalesce.go | 308 +++++++ internal/chart/v3/util/coalesce_test.go | 723 +++++++++++++++ internal/chart/v3/util/compatible.go | 34 + internal/chart/v3/util/compatible_test.go | 43 + internal/chart/v3/util/create.go | 832 ++++++++++++++++++ internal/chart/v3/util/create_test.go | 172 ++++ internal/chart/v3/util/dependencies.go | 366 ++++++++ internal/chart/v3/util/dependencies_test.go | 569 ++++++++++++ internal/chart/v3/util/doc.go | 45 + internal/chart/v3/util/errors.go | 43 + internal/chart/v3/util/errors_test.go | 37 + internal/chart/v3/util/expand.go | 94 ++ internal/chart/v3/util/expand_test.go | 124 +++ internal/chart/v3/util/jsonschema.go | 113 +++ internal/chart/v3/util/jsonschema_test.go | 247 ++++++ internal/chart/v3/util/save.go | 253 ++++++ internal/chart/v3/util/save_test.go | 261 ++++++ .../Chart.yaml | 14 + .../charts/child/Chart.yaml | 6 + .../charts/child/charts/grandchild/Chart.yaml | 6 + .../charts/grandchild/templates/dummy.yaml | 7 + .../charts/child/templates/dummy.yaml | 7 + .../values.yaml | 7 + .../Chart.yaml | 20 + .../charts/child/Chart.yaml | 12 + .../charts/child/charts/grandchild/Chart.yaml | 6 + .../child/charts/grandchild/values.yaml | 2 + .../charts/child/templates/dummy.yaml | 7 + .../templates/dummy.yaml | 7 + .../chart/v3/util/testdata/chartfiletest.yaml | 20 + .../chart/v3/util/testdata/coleridge.yaml | 12 + .../dependent-chart-alias/.helmignore | 1 + .../testdata/dependent-chart-alias/Chart.lock | 8 + .../testdata/dependent-chart-alias/Chart.yaml | 29 + .../dependent-chart-alias/INSTALL.txt | 1 + .../testdata/dependent-chart-alias/LICENSE | 1 + .../testdata/dependent-chart-alias/README.md | 11 + .../dependent-chart-alias/charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 967 bytes .../dependent-chart-alias/docs/README.md | 1 + .../testdata/dependent-chart-alias/icon.svg | 8 + .../dependent-chart-alias/ignore/me.txt | 0 .../templates/template.tpl | 1 + .../dependent-chart-alias/values.yaml | 6 + .../dependent-chart-helmignore/.helmignore | 2 + .../dependent-chart-helmignore/Chart.yaml | 17 + .../charts/.ignore_me | 0 .../charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../templates/template.tpl | 1 + .../dependent-chart-helmignore/values.yaml | 6 + .../.helmignore | 1 + .../Chart.yaml | 17 + .../INSTALL.txt | 1 + .../LICENSE | 1 + .../README.md | 11 + .../charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 967 bytes .../docs/README.md | 1 + .../icon.svg | 8 + .../ignore/me.txt | 0 .../templates/template.tpl | 1 + .../values.yaml | 6 + .../.helmignore | 1 + .../Chart.yaml | 24 + .../INSTALL.txt | 1 + .../LICENSE | 1 + .../README.md | 11 + .../charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 967 bytes .../docs/README.md | 1 + .../icon.svg | 8 + .../ignore/me.txt | 0 .../templates/template.tpl | 1 + .../values.yaml | 6 + .../.helmignore | 1 + .../Chart.yaml | 21 + .../INSTALL.txt | 1 + .../LICENSE | 1 + .../README.md | 11 + .../charts/_ignore_me | 1 + .../charts/alpine/Chart.yaml | 5 + .../charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../charts/alpine/values.yaml | 2 + .../charts/mariner-4.3.2.tgz | Bin 0 -> 967 bytes .../docs/README.md | 1 + .../icon.svg | 8 + .../ignore/me.txt | 0 .../templates/template.tpl | 1 + .../values.yaml | 6 + .../chart/v3/util/testdata/frobnitz-1.2.3.tgz | Bin 0 -> 3485 bytes .../v3/util/testdata/frobnitz/.helmignore | 1 + .../v3/util/testdata/frobnitz/Chart.lock | 8 + .../v3/util/testdata/frobnitz/Chart.yaml | 27 + .../v3/util/testdata/frobnitz/INSTALL.txt | 1 + .../chart/v3/util/testdata/frobnitz/LICENSE | 1 + .../chart/v3/util/testdata/frobnitz/README.md | 11 + .../util/testdata/frobnitz/charts/_ignore_me | 1 + .../frobnitz/charts/alpine/Chart.yaml | 5 + .../testdata/frobnitz/charts/alpine/README.md | 9 + .../charts/alpine/charts/mast1/Chart.yaml | 5 + .../charts/alpine/charts/mast1/values.yaml | 4 + .../charts/alpine/charts/mast2-0.1.0.tgz | Bin 0 -> 252 bytes .../charts/alpine/templates/alpine-pod.yaml | 14 + .../frobnitz/charts/alpine/values.yaml | 2 + .../frobnitz/charts/mariner/Chart.yaml | 9 + .../mariner/charts/albatross/Chart.yaml | 5 + .../mariner/charts/albatross/values.yaml | 4 + .../charts/mariner/templates/placeholder.tpl | 1 + .../frobnitz/charts/mariner/values.yaml | 7 + .../v3/util/testdata/frobnitz/docs/README.md | 1 + .../chart/v3/util/testdata/frobnitz/icon.svg | 8 + .../v3/util/testdata/frobnitz/ignore/me.txt | 0 .../testdata/frobnitz/templates/template.tpl | 1 + .../v3/util/testdata/frobnitz/values.yaml | 6 + .../testdata/frobnitz_backslash-1.2.3.tgz | Bin 0 -> 3496 bytes internal/chart/v3/util/testdata/genfrob.sh | 14 + .../parent-chart/Chart.lock | 9 + .../parent-chart/Chart.yaml | 22 + .../parent-chart/charts/dev-v0.1.0.tgz | Bin 0 -> 333 bytes .../parent-chart/charts/prod-v0.1.0.tgz | Bin 0 -> 336 bytes .../parent-chart/envs/dev/Chart.yaml | 4 + .../parent-chart/envs/dev/values.yaml | 9 + .../parent-chart/envs/prod/Chart.yaml | 4 + .../parent-chart/envs/prod/values.yaml | 9 + .../parent-chart/templates/autoscaler.yaml | 16 + .../parent-chart/values.yaml | 10 + .../chart/v3/util/testdata/joonix/Chart.yaml | 4 + .../v3/util/testdata/joonix/charts/.gitkeep | 0 .../chart/v3/util/testdata/subpop/Chart.yaml | 41 + .../chart/v3/util/testdata/subpop/README.md | 18 + .../subpop/charts/subchart1/Chart.yaml | 36 + .../subchart1/charts/subchartA/Chart.yaml | 4 + .../charts/subchartA/templates/service.yaml | 15 + .../subchart1/charts/subchartA/values.yaml | 17 + .../subchart1/charts/subchartB/Chart.yaml | 4 + .../charts/subchartB/templates/service.yaml | 15 + .../subchart1/charts/subchartB/values.yaml | 35 + .../subpop/charts/subchart1/crds/crdA.yaml | 13 + .../charts/subchart1/templates/NOTES.txt | 1 + .../charts/subchart1/templates/service.yaml | 22 + .../subchart1/templates/subdir/role.yaml | 7 + .../templates/subdir/rolebinding.yaml | 12 + .../templates/subdir/serviceaccount.yaml | 4 + .../subpop/charts/subchart1/values.yaml | 55 ++ .../subpop/charts/subchart2/Chart.yaml | 19 + .../subchart2/charts/subchartB/Chart.yaml | 4 + .../charts/subchartB/templates/service.yaml | 15 + .../subchart2/charts/subchartB/values.yaml | 21 + .../subchart2/charts/subchartC/Chart.yaml | 4 + .../charts/subchartC/templates/service.yaml | 15 + .../subchart2/charts/subchartC/values.yaml | 21 + .../charts/subchart2/templates/service.yaml | 15 + .../subpop/charts/subchart2/values.yaml | 21 + .../v3/util/testdata/subpop/noreqs/Chart.yaml | 4 + .../subpop/noreqs/templates/service.yaml | 15 + .../util/testdata/subpop/noreqs/values.yaml | 26 + .../chart/v3/util/testdata/subpop/values.yaml | 45 + .../testdata/test-values-invalid.schema.json | 1 + .../util/testdata/test-values-negative.yaml | 14 + .../v3/util/testdata/test-values.schema.json | 67 ++ .../chart/v3/util/testdata/test-values.yaml | 17 + .../three-level-dependent-chart/README.md | 16 + .../umbrella/Chart.yaml | 19 + .../umbrella/charts/app1/Chart.yaml | 11 + .../charts/app1/charts/library/Chart.yaml | 5 + .../charts/library/templates/service.yaml | 9 + .../charts/app1/charts/library/values.yaml | 5 + .../charts/app1/templates/service.yaml | 1 + .../umbrella/charts/app1/values.yaml | 3 + .../umbrella/charts/app2/Chart.yaml | 11 + .../charts/app2/charts/library/Chart.yaml | 5 + .../charts/library/templates/service.yaml | 9 + .../charts/app2/charts/library/values.yaml | 5 + .../charts/app2/templates/service.yaml | 1 + .../umbrella/charts/app2/values.yaml | 3 + .../umbrella/charts/app3/Chart.yaml | 11 + .../charts/app3/charts/library/Chart.yaml | 5 + .../charts/library/templates/service.yaml | 9 + .../charts/app3/charts/library/values.yaml | 5 + .../charts/app3/templates/service.yaml | 1 + .../umbrella/charts/app3/values.yaml | 2 + .../umbrella/charts/app4/Chart.yaml | 9 + .../charts/app4/charts/library/Chart.yaml | 5 + .../charts/library/templates/service.yaml | 9 + .../charts/app4/charts/library/values.yaml | 5 + .../charts/app4/templates/service.yaml | 1 + .../umbrella/charts/app4/values.yaml | 3 + .../umbrella/values.yaml | 14 + internal/chart/v3/util/validate_name.go | 111 +++ internal/chart/v3/util/validate_name_test.go | 91 ++ internal/chart/v3/util/values.go | 220 +++++ internal/chart/v3/util/values_test.go | 293 ++++++ 373 files changed, 10070 insertions(+) create mode 100644 internal/chart/v3/chart.go create mode 100644 internal/chart/v3/chart_test.go create mode 100644 internal/chart/v3/dependency.go create mode 100644 internal/chart/v3/dependency_test.go create mode 100644 internal/chart/v3/doc.go create mode 100644 internal/chart/v3/errors.go create mode 100644 internal/chart/v3/file.go create mode 100644 internal/chart/v3/fuzz_test.go create mode 100644 internal/chart/v3/loader/archive.go create mode 100644 internal/chart/v3/loader/archive_test.go create mode 100644 internal/chart/v3/loader/directory.go create mode 100644 internal/chart/v3/loader/load.go create mode 100644 internal/chart/v3/loader/load_test.go create mode 100644 internal/chart/v3/loader/testdata/LICENSE create mode 100644 internal/chart/v3/loader/testdata/albatross/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/albatross/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz-1.2.3.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl create mode 100644 internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/.helmignore create mode 100644 internal/chart/v3/loader/testdata/frobnitz/Chart.lock create mode 100644 internal/chart/v3/loader/testdata/frobnitz/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz/LICENSE create mode 100644 internal/chart/v3/loader/testdata/frobnitz/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/_ignore_me create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz/docs/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz/icon.svg create mode 100644 internal/chart/v3/loader/testdata/frobnitz/ignore/me.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl create mode 100644 internal/chart/v3/loader/testdata/frobnitz/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/README.md create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/docs/README.md create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/icon.svg create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl create mode 100755 internal/chart/v3/loader/testdata/frobnitz_backslash/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/docs/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/icon.svg create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/docs/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/icon.svg create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt create mode 120000 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/docs/README.md create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/icon.svg create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl create mode 100644 internal/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml create mode 100755 internal/chart/v3/loader/testdata/genfrob.sh create mode 100644 internal/chart/v3/loader/testdata/mariner/Chart.yaml create mode 100644 internal/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz create mode 100644 internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl create mode 100644 internal/chart/v3/loader/testdata/mariner/values.yaml create mode 100644 internal/chart/v3/metadata.go create mode 100644 internal/chart/v3/metadata_test.go create mode 100644 internal/chart/v3/util/capabilities.go create mode 100644 internal/chart/v3/util/capabilities_test.go create mode 100644 internal/chart/v3/util/chartfile.go create mode 100644 internal/chart/v3/util/chartfile_test.go create mode 100644 internal/chart/v3/util/coalesce.go create mode 100644 internal/chart/v3/util/coalesce_test.go create mode 100644 internal/chart/v3/util/compatible.go create mode 100644 internal/chart/v3/util/compatible_test.go create mode 100644 internal/chart/v3/util/create.go create mode 100644 internal/chart/v3/util/create_test.go create mode 100644 internal/chart/v3/util/dependencies.go create mode 100644 internal/chart/v3/util/dependencies_test.go create mode 100644 internal/chart/v3/util/doc.go create mode 100644 internal/chart/v3/util/errors.go create mode 100644 internal/chart/v3/util/errors_test.go create mode 100644 internal/chart/v3/util/expand.go create mode 100644 internal/chart/v3/util/expand_test.go create mode 100644 internal/chart/v3/util/jsonschema.go create mode 100644 internal/chart/v3/util/jsonschema_test.go create mode 100644 internal/chart/v3/util/save.go create mode 100644 internal/chart/v3/util/save_test.go create mode 100644 internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml create mode 100644 internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml create mode 100644 internal/chart/v3/util/testdata/chartfiletest.yaml create mode 100644 internal/chart/v3/util/testdata/coleridge.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/Chart.lock create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl create mode 100644 internal/chart/v3/util/testdata/dependent-chart-alias/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl create mode 100644 internal/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl create mode 100644 internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl create mode 100644 internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz-1.2.3.tgz create mode 100644 internal/chart/v3/util/testdata/frobnitz/.helmignore create mode 100644 internal/chart/v3/util/testdata/frobnitz/Chart.lock create mode 100644 internal/chart/v3/util/testdata/frobnitz/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/INSTALL.txt create mode 100644 internal/chart/v3/util/testdata/frobnitz/LICENSE create mode 100644 internal/chart/v3/util/testdata/frobnitz/README.md create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/_ignore_me create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/README.md create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl create mode 100644 internal/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz/docs/README.md create mode 100644 internal/chart/v3/util/testdata/frobnitz/icon.svg create mode 100644 internal/chart/v3/util/testdata/frobnitz/ignore/me.txt create mode 100644 internal/chart/v3/util/testdata/frobnitz/templates/template.tpl create mode 100644 internal/chart/v3/util/testdata/frobnitz/values.yaml create mode 100644 internal/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz create mode 100755 internal/chart/v3/util/testdata/genfrob.sh create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml create mode 100644 internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml create mode 100644 internal/chart/v3/util/testdata/joonix/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/joonix/charts/.gitkeep create mode 100644 internal/chart/v3/util/testdata/subpop/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/README.md create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/noreqs/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/noreqs/values.yaml create mode 100644 internal/chart/v3/util/testdata/subpop/values.yaml create mode 100644 internal/chart/v3/util/testdata/test-values-invalid.schema.json create mode 100644 internal/chart/v3/util/testdata/test-values-negative.yaml create mode 100644 internal/chart/v3/util/testdata/test-values.schema.json create mode 100644 internal/chart/v3/util/testdata/test-values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/README.md create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml create mode 100644 internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml create mode 100644 internal/chart/v3/util/validate_name.go create mode 100644 internal/chart/v3/util/validate_name_test.go create mode 100644 internal/chart/v3/util/values.go create mode 100644 internal/chart/v3/util/values_test.go diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go new file mode 100644 index 000000000..4d59fa5ec --- /dev/null +++ b/internal/chart/v3/chart.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 v3 + +import ( + "path/filepath" + "regexp" + "strings" +) + +// 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 []*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 []*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"` + // Files are miscellaneous files in a chart archive, + // e.g. README, LICENSE, etc. + Files []*File `json:"files"` + + 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 *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() []*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") +} diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go new file mode 100644 index 000000000..f93b3356b --- /dev/null +++ b/internal/chart/v3/chart_test.go @@ -0,0 +1,211 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + + "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: "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) { + 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/internal/chart/v3/dependency.go b/internal/chart/v3/dependency.go new file mode 100644 index 000000000..2d956b548 --- /dev/null +++ b/internal/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/internal/chart/v3/dependency_test.go b/internal/chart/v3/dependency_test.go new file mode 100644 index 000000000..fcea19aea --- /dev/null +++ b/internal/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/internal/chart/v3/doc.go b/internal/chart/v3/doc.go new file mode 100644 index 000000000..e003833a0 --- /dev/null +++ b/internal/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/internal/chart/v3/errors.go b/internal/chart/v3/errors.go new file mode 100644 index 000000000..059e43f07 --- /dev/null +++ b/internal/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/internal/chart/v3/file.go b/internal/chart/v3/file.go new file mode 100644 index 000000000..ba04e106d --- /dev/null +++ b/internal/chart/v3/file.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 v3 + +// 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/internal/chart/v3/fuzz_test.go b/internal/chart/v3/fuzz_test.go new file mode 100644 index 000000000..982c26489 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/archive.go b/internal/chart/v3/loader/archive.go new file mode 100644 index 000000000..311959d56 --- /dev/null +++ b/internal/chart/v3/loader/archive.go @@ -0,0 +1,234 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +// 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]:/`) + +// FileLoader loads a chart from a file +type FileLoader string + +// Load loads a chart +func (l FileLoader) Load() (*chart.Chart, error) { + return LoadFile(string(l)) +} + +// LoadFile loads from an archive file. +func LoadFile(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 = ensureArchive(name, raw) + if err != nil { + return nil, err + } + + c, err := LoadArchive(raw) + 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 archive 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) + remainingSize := MaxDecompressedChartSize + 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, 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, 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) (*chart.Chart, error) { + files, err := LoadArchiveFiles(in) + if err != nil { + return nil, err + } + + return LoadFiles(files) +} diff --git a/internal/chart/v3/loader/archive_test.go b/internal/chart/v3/loader/archive_test.go new file mode 100644 index 000000000..d16c47563 --- /dev/null +++ b/internal/chart/v3/loader/archive_test.go @@ -0,0 +1,92 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "testing" +) + +func TestLoadArchiveFiles(t *testing.T) { + tcs := []struct { + name string + generate func(w *tar.Writer) + check func(t *testing.T, files []*BufferedFile, err error) + }{ + { + name: "empty input should return no files", + 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) + } + }, + }, + { + name: "should ignore files with XGlobalHeader type", + generate: func(w *tar.Writer) { + // simulate the presence of a `pax_global_header` file like you would get when + // processing a GitHub release archive. + err := w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeXGlobalHeader, + Name: "pax_global_header", + }) + if err != nil { + t.Fatal(err) + } + + // we need to have at least one file, otherwise we'll get the "no files in chart archive" error + err = w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: "dir/empty", + }) + if err != nil { + t.Fatal(err) + } + }, + 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) + } + + if len(files) != 1 { + t.Fatalf(`expected to get one file but got [%v]`, files) + } + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + buf := &bytes.Buffer{} + gzw := gzip.NewWriter(buf) + tw := tar.NewWriter(gzw) + + tc.generate(tw) + + _ = tw.Close() + _ = gzw.Close() + + files, err := LoadArchiveFiles(buf) + tc.check(t, files, err) + }) + } +} diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go new file mode 100644 index 000000000..947051604 --- /dev/null +++ b/internal/chart/v3/loader/directory.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 loader + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/sympath" + "helm.sh/helm/v4/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() (*chart.Chart, error) { + return LoadDir(string(l)) +} + +// LoadDir loads from a directory. +// +// This loads charts only from directories. +func LoadDir(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 := []*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() > MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), 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, &BufferedFile{Name: n, Data: data}) + return nil + } + if err = sympath.Walk(topdir, walk); err != nil { + return c, err + } + + return LoadFiles(files) +} diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go new file mode 100644 index 000000000..30bafdad4 --- /dev/null +++ b/internal/chart/v3/loader/load.go @@ -0,0 +1,219 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "errors" + "fmt" + "io" + "maps" + "os" + "path/filepath" + "strings" + + utilyaml "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/yaml" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +// ChartLoader loads a chart. +type ChartLoader interface { + Load() (*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(name string) (*chart.Chart, error) { + l, err := Loader(name) + if err != nil { + return nil, err + } + return l.Load() +} + +// BufferedFile represents an archive file buffered for later processing. +type BufferedFile struct { + Name string + Data []byte +} + +// LoadFiles loads from in-memory files. +func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { + c := new(chart.Chart) + subcharts := make(map[string][]*BufferedFile) + + // 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, 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 + } + } + } + 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 + + 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 + } + + fname := strings.TrimPrefix(f.Name, "charts/") + cname := strings.SplitN(fname, "/", 2)[0] + subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) + default: + c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data}) + } + } + + if c.Metadata == nil { + return c, errors.New("Chart.yaml file is missing") //nolint:staticcheck + } + + if err := c.Validate(); err != nil { + return c, err + } + + 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) + } + // Untar the chart and add to c.Dependencies + sc, err = LoadArchive(bytes.NewBuffer(file.Data)) + 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) + } + sc, err = LoadFiles(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 +} + +// 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 a 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 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/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go new file mode 100644 index 000000000..e770923ff --- /dev/null +++ b/internal/chart/v3/loader/load_test.go @@ -0,0 +1,711 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "io" + "log" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" + "testing" + "time" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +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() + 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(); 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() + 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 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() + 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() + 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() + 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) { + goodFiles := []*BufferedFile{ + { + Name: "Chart.yaml", + 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", + Data: []byte("var: some values"), + }, + { + Name: "values.schema.json", + Data: []byte("type: Values"), + }, + { + Name: "templates/deployment.yaml", + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + Data: []byte("some service"), + }, + } + + c, err := LoadFiles(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([]*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) { + goodFiles := []*BufferedFile{ + { + Name: "requirements.yaml", + Data: []byte("dependencies:"), + }, + { + Name: "values.yaml", + Data: []byte("var: some values"), + }, + + { + Name: "templates/deployment.yaml", + Data: []byte("some deployment"), + }, + { + Name: "templates/service.yaml", + Data: []byte("some service"), + }, + { + Name: "Chart.yaml", + 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(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("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() + 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) + 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) + 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) + 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) + 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 TestMergeValues(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 []*chart.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/internal/chart/v3/loader/testdata/LICENSE b/internal/chart/v3/loader/testdata/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/loader/testdata/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/loader/testdata/albatross/Chart.yaml b/internal/chart/v3/loader/testdata/albatross/Chart.yaml new file mode 100644 index 000000000..eeef737ff --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/albatross/values.yaml b/internal/chart/v3/loader/testdata/albatross/values.yaml new file mode 100644 index 000000000..3121cd7ce --- /dev/null +++ b/internal/chart/v3/loader/testdata/albatross/values.yaml @@ -0,0 +1,4 @@ +albatross: "true" + +global: + author: Coleridge diff --git a/internal/chart/v3/loader/testdata/frobnitz-1.2.3.tgz b/internal/chart/v3/loader/testdata/frobnitz-1.2.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..de28e4120df739fc1e9a246cfce150ccb1bcea1e GIT binary patch literal 3420 zcmV-i4WsfOiwFP!000001MOT3coRh)51?=cc|Q(>gCXVPA?-HL4Xte1H?VZrX;Y&;t=T0r3+Qp`cQbhst+|p3tv&C+7)zB2)oI5d{kfViit#w7uEg zByB@60%?l4zYn^bo!Ob${m<{e|Ns1F#?G-;h7xAPs_G9~1hdH`9(87uw&u|fmCmF! z;3mCZi{mP-PH!?8Rlrn_Z3@7{3j_y%%4H`wiXm>AFI4*P6n4M-F(;&5l!IY8816bm z0pmvF&E>BPmcJ24-34gF-H^KQ2baGr@mO_3w?je0Ai)1*C&39A&DwIpUPmbY8??G$ z{u_-th5wNN3Ehw(A!DN;AFl==1~7yR#sh+OQw&5G0LXLmpN+W&wait2r%;oSw{etP zkdntZDITDIL?9hgS2J0M=`n*woddmjEJyluNuVI{2k++4*98jF50WJbQtcurMj%AJ zP)qrYaY8T@nWJC~BwUhmfG8)8^a8p#u!d5oaZe`ef-D0;D$T>1w=k@gCB3z7;#m)8 z^Q*hMRE?cwW32?IcCup0VvgDw)Rawp=DKdhjrq6{b2AP#!7!{qh_6nn3FZqNf!lM{Fm7GC3h9CHT(eq~D!lZs?w@;C2wu|7pWB$5_(SF(F?FcO4=9KePS@`%kM^ z^nWA}Q0fe_sL-pRX{4HM9v5T;Owz+PnnLGDOLzfE@opL&1#}Ol@co`ZfdA=f$*IFL zQ|qISko?!1g7|MR8E}RFk$_L^01?{;oh(g4PNDW62m$_&PPL?Dq+%{I{B?xlzus`e z_z$uP6#hp7QDCs&M^o3Z1$_2BkZgNQt&WNUV|l^>>&k?IGyymX9=Te;BW9BGr@&lD zvu@FnP^$;C93bTjkVBS+os9-OgvgXY|57 zO8hSp2$p}A91zNKg)s^kO8@J$c#!;YtvpA_h(SuaxgR)VK&z#?S<=@jG12X2Aeh`PghgQXI62ZalhEitma+ zW27LwfTyI2kitODz8r@Id6eJ;gxbb>#3a@Z+ysZzkbo45zi=re%3}iSqO*aW5(g&y z1&i$?p`Gy10+6!-JIjfe)!A|rV^D3;mmk-k0gDYh zaYG9&jJOH^!#Wyp>-L|_f5rZ*fBd@qT?8-a?zk7+y8O+!R;S2668J6hzf)%*r2TI& zR@?t(y-KS`4xlpsJ0hsl{+GmF+x)l46SAa!A$?$nd909Q&{vwT+w|9H?xoM6o<@mRvavL6Fe^s6{DYCWbFqGa9+Gwya^-%jdY))E*b=(`LBl`lb>!GqWms0OZsI~sQy-=|_` z#Z%usx;1)4&!V?Jm~gRR`UC5da*iK=LOb=1o@HBi^*yzI-}AH1Zo%HInj5$2v;99B zyK;x)|8ZvgsfX03Qf<*4^uw2R!n%O5ec##GEb7zIv>3-kR%GeLuW1 zcR|IxpZ0gMojB78J!sW81*KW42OIds-zB&S|EouH!cfO;_rmxxg;GA9_q0m&Gvw^nJHkjtfPashSS7N|a` zG64K>C8_fwpED>=bpw6RID586&AXvZY#;|k$qOS{nzBjl@sA>FhC}4hY5?ZTk>!@M zNLBiVorBgZ{k0i18S)EBnTOG%jz9+z}M7>>fuj|9z_g zW2E(eci{8YLHqrQG)edS2Y z=A*j#dH9<1_rEfJPoD|>N}E${Hl5vh?Cn*%S5Dj4JhlCpl=8jT8h5a4n^dtOe`JR? zZ2JxgZ53@&)~#i#=p(|UBZbWAOK zXnE?ZCrwZG2Hc*WO$%P=>3wV9^0NC|waz^DN>5#va30c3ss=&Hnljh<@X8gi-@Xu(IDT2Jq#?t@2b@o|fy|3Z)k_(G+r-9|0{i&gc*ycE^uj)UmVyD}=9>>X4@^gH_c{$#Lt2ld4DImRy2iWVPT zwBgE1zO=>K{EbQLSJ|3Gm%+;a^{?D&?Dlyn-K6AV)t>gJHdk#b=yml-Y#wuB?6L z?JX;NpPMnf|KA9poEdlRWbfA#D~(@XsVvU;By+DWt$R5!%z1SCXD7EZ zN5(QQe7ej2{ZqwHw0vjr5}SS8rTEj64L_WDeyYdpD31AM^zKvTM~}buuZQ(J=l8zw zL6Z|d7Ckeoc;%{5*J8fo5`H@L?Uv17d{;5{N-Fisl$V7slQ!CO-fIT`-J-bM89nRL zwl#Uz-)p&U&e`!_UtB(=s?&-K9iJ{vxPS8Ai`Po_wi~v-vaso#PYb7L`b^uL`XB3# zV{Ok4jt{p(`bgnoR`Ci?T26H|M? zd3x{%jW2(`qTPV$_OCoyKk3s8zKnCIUvAs75xuYVM?KULivK1&c>Rx2ng1OjBn-%R(f)m35;fvBG6Ry- z38*6gl)&)u=qyp=+X5#qFAvKzU@Yf|)g!sZqBM=1?N_r%17LD#`837k)RkEm7Z(>R zrD|&KL?gEdnq-(#{8E`x`ta8fEnXOc=WhA4%D zq*zFT_(GEIB4t|5e|aOw`l#d9?LQ;RpxA$rKtTRsjri5ZZOGqb&@1wf1nQ7~V3NGx z4$oeIp~wG&_kS{Lab^Ffh+rtBX;zG_i4ij}E3lM8lgDy2shK_dF8Wp}2tNY+4{OA) zHbU`VcSHQoXx1s~|09AZUY%X%_b0ag+{GLR85P@q780tE^h0RIOcqcM5_cmM#VXTU=M literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/Chart.yaml new file mode 100644 index 000000000..1b63fc3e2 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/_ignore_me @@ -0,0 +1 @@ +This should be ignored by the loader, but may be included in a chart. diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..21ae20aad --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.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/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3190136b050e62c628b3c817fd963ac9dc4a9e25 GIT binary patch literal 967 zcmV;&133I2iwFR+9h6)E1MQb_y%a`Bd>#= zhbqf2w!b6}wZ9@rvIhtwzm?~C&+QLm+G2!l%`$_aUgS(@pdjdX3a%E}VXVc7`)dg( zL%IRN2}c1D3xoMi2w@WuWOMZ?5i&3FelBVyq_Ce_8fREdP%NDf<&d!wu3{&VUQNs{N%vK$Ha~k^gB2!0bO7r0ic0 zbqCp*X#j?=|H@GNONsuE)&Idbh>2MX(Z}|+=@*sLod*wS?A8Ugp#mMPuMO0NnZmos9_rr z3xpDL+otj~lRh?B4hHFTl+d5-8NBXmUXB~EyF^bx7m*+!*g>obczvGF|8xkWsHN8; z%#+wiWP{=2pC*98@$VNzzsll&G#D7&11-;D>HT0x|DV2?6}a~*p46@R|2l??e_8dX z@Bb>D3t~VC_*wjq2Dy!6J-_5ME%%J+xmsbK5a#qO>ZV?Wt`d|TDdrB^B&LqFr@lYdUA zs&4-JAg3&uc4dA0gv*bXU9QePa9^8wR{HovA)j@)JOAIkak0Jl)aKqA_3XLM#?H=V z-{tlG=xy^os=1Z><53;&+C4=zp|%rwv!)UyY=%k_t%Y|wNRQl`C6N_Z&S;S+~wb1=)2Uh_4I81`y>DO zoLPSt=btB&x{CUK`0DA&){efW_O36RxnsYZNT0r;JbTLR{H4M3C$8(Cee==aQ~Gbq p$`5v3?NB{4-j0XQHf literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz.v3.reqs/values.yaml @@ -0,0 +1,6 @@ +# A values file contains configuration. + +name: "Some Name" + +section: + name: "Name in a section" diff --git a/internal/chart/v3/loader/testdata/frobnitz/.helmignore b/internal/chart/v3/loader/testdata/frobnitz/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/loader/testdata/frobnitz/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz/Chart.lock new file mode 100644 index 000000000..6fcc2ed9f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz/Chart.yaml new file mode 100644 index 000000000..1b63fc3e2 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/loader/testdata/frobnitz/LICENSE b/internal/chart/v3/loader/testdata/frobnitz/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/loader/testdata/frobnitz/README.md b/internal/chart/v3/loader/testdata/frobnitz/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..21ae20aad --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5c6bc4dcb370466921bbde3c433614bed548313d GIT binary patch literal 910 zcmV;919AKxiwFP!000001MQe;XcIvgh9ly&p#D%nsMs-rcQ%{s1*uoOC|E1h3uzIj z>9pCpm%Ec{#S29gR7CN>`vMg~tXln{cu*A)Zxj_g@kdp#o~;@WbarD(Tah9*5pf;@ zJDZ)IZd$kcAQSzCpeT6& z*YaKYng3jWXeyJDWh;gr0%bg-Lk)$%k4eE4Avj{C+eWYNm?Vh@ttDbJPu;c%?p|mtzAg=Vku(IR2|N9^2Gx1HbS8ycFc9|EGf{ z`qwW^pS!MDTr%g+V>IXg0v~ksmt}z?djQd2l4a`uX(4lY`$VC2&8NiuM0kR z%`9J-dn5{G?Uol0Iltky)T+zUO;r!)Uzz^A-#~QzMdz&38UD5VdW^p-ZdtVI4V%AW z@czneV;@q_uD zl)9>uC+d1mIJ0Mvzxp5(TfabAdpY~zC$K&KG~W6Cy-n3qz%fZ5@nK77*sa>(t8Uj- zZk*P(X6}x?hYrDti(_V1^g88RGw|q|U3E8Sy)F1syX5iM+^@qbGPHsx?aTw;@}`u0 zy_1(w`z`BoJoD1#s;J%T{Z;uzY4}{EblyItW^2a>WyTG?@n|f3^sJkA{KpQSe(6`@ z?~q*o7?JH3lD#|yTfp!8zrYhqp#Gl*e%7B{A}DI8MaW{Y*%IkMbN){ffF}COd{CzT zpALG1iBZJ{?3Y|3Py7mq9g#?9?0pG@t*AP5oaC(C@#r&>M_G#W1E~keC5( zW + + Example icon + + + diff --git a/internal/chart/v3/loader/testdata/frobnitz/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/loader/testdata/frobnitz/values.yaml b/internal/chart/v3/loader/testdata/frobnitz/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz b/internal/chart/v3/loader/testdata/frobnitz_backslash-1.2.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..dfbe88a735b256fc6c82c62beea78c78b557fc0c GIT binary patch literal 3434 zcmV-w4VCgAiwFP!000001MOT3coS6?4pd&OTqZs10a>2@fr?B3tf6NYP z2W4Ye4u+mSFBIt5UNrukpSA6)+)J;x}UyB{)|2Lbq>WG6TQqghK6c5lpqE-@9)^S>cX)&EjnFZZBEJu2+ice7b!8=2Ab?kJc935R4RAd;k!g+K>~)iV$E&Pit-kYyl9rg>QZ6oxgk zq`SdMJnP~tKEoHt=vY}c#!PTZJ1f>KN;DKuQWo)=1D%TNb8$W9WNb=;VOW6>Z=KW< z%oR8S-JLkk9x`Jtv7ZDIvO`WP@a}txes?Cixu0nnza9bXf6CbONv70POvn{N(?-zv z4;g!Z|4*xy?SD8>YswkqQK4Hw)5suOTn@+xm}Gq|G==t&ZEypU;+-_w3g{e6X8B!# z0Q^r)NlG4@o*cS1g7RN$@Z-P1V9?9_4+p%;28h@eXlH2>ax%65KnTG9iOHrBX~~#_ z3{@M!_^;L7GX5i)1eyO~KqMIHi_Dbud;zal4hH0u;S38iu*%K_4J0CLFFu(DBrhY-0E=s)X>rp2;)@r7)I!h`_)_e60a zOanGP|A(t}IywIr4%}Y<8J^TohT}J90fXsZqsANRU#nK*8rlAb1ClxC?W~I?ff)ji z&e5)0GR-tgAcrstm|dO-P?TUSw2MR{6axs~`=Pn7LPk?@d;WWd7enzHXuSP5>eRCR z4-10oKLpvJp!Ofv)#ZQmNKUKK>g4giupnUm$D3Jbknj08)#u24uT;;rpRAl;UFsa8i`Cy91?&aQ-G)nbexaL1SG{l zix|eEGo`Gqm?}mF!VP#zY6uw&6zqGluONpK?0`^OSeIDDT7i?`kQow?LGe{CB|>>j zU>$TOuu|f{gs)<;eYx1r|}sfJ!<~ zHW$r;#_(SoEg5)Bu+3{!8^QRmGwS{MuU5r-UvTP^H}k(+y=UEhiea1K z-j>~`4y>AA_2jpY?1~!Kukg+Hr&Z+5y>D}3*6Guat7pI7uYA}3L1kMGJ-6V(PVAkU z#j)EzJ^X{d7k51F@8_qMJ)kU0wnTN+j$P9o>j@?gdgs7QbNeCXi`A}ii*~mD^3m!g z8*kPO`rz8)WmQXlJlx%K=6rW_p*3IUm1ZdJZ{{0+haf2b>&A7O+(r}PKXFNz9RGy@ zjnKc}XxguDBbfeit-t>DT8-@gg#``bKaDbrV#qi!o+XtIC=ete5aNmxPgFR6J~k23 zkl-OU79Ci)Fd7~4j0kvsBT1StLt0v;0s!H3Vp+9OB1W;uiC_pDB%{sl2DPO_E}ODI zumH?;u>u5AO@Yp@?T25h0QeG2(zA<#Pd}Nuo_a5K;X;*?cS4KUQx1xcfH0n=DT~xG z-*B>OEJUHL3SeG|45uU_HR&CH_FJx$Lr{Wu0S)Cqmx_AgVNIM4i}kFb@s6d(ftjip zGsVR4G=xr7lqwbxK{Y$aor zj`pZuj>_(G+@Am5)qv5``oBiUeDx88{~BE5ng3@r>eaY3{;S4~GXKK@6#sd4A}(*B z?43>Bmq&i}`j+bPrX43WOLOpzm+pOe>cN522A8&>I&QzP@6=oC53HMWs7-R0Nh2yt zZnW%b+C8IcYwq~29oa5j<10r^er@xf2ignSmPcl;RpA}y_HOy`#!Z{p4LRRzsv4R1 z%nOl6DpLCPGIdKXe;_~km9vH?1_170zgBtA_jA8FEWiBT_8rnsz1&aJGpUVY&6G>4 z@3{$!zS&&!qJ8L_l8H9loU)Pn@6 zgYWgf=NVgGYN>NZ<-C-L{>N73_nwgdpjq+#k`am>-GB7M&}g)2|4*m`XpU)pw4?zY4&>n#yc<*@p{L#lV_dw*6+M-*4o9PCoIqh@Z=u^~KlaAi0k0)g>%X{GU6l55dWj~b zPbD$Nesa&JXLm6tCNs}}vfujMlSPlWdwb<7i*?GCxN|dg-=BYOw##TMivD@xfwIbz zr(gZ&L)v{y2V8zX;>-_)PcJB1w|>Hn=x@3BACG;rbI0f3R!zQ^O#M9TCE<(2ZPu)J zTf={~EvmFfEx59KW6sTY+ihNSVd_^E`Lk-euf5#usiOFMXO>jlC@$$dW=nNJt3{s_ z%u)@Uvm^Q6=DnvnT^Pn!kL&bU|LaErWSrc>AMiTQ}~tB&9EWsC3NfPG8^i#EzrGnd=1`vP%B45=PLE z4c|_G_EJLffH%&Ke81(@&(?MxI@kK8E8|CPYTg&IHswp5+O?n$b+{U(TsC~`_<>z~ zn{ApiEn1u`%Tx?(4*mzQ|0HV(Oj#jdK1!EV!NiL%9ensQwMNtp5_l<@bLW zP?zDUw*pj30{NDKOCPSdtL8k(s6zn$i=Q;X_-tFq+6cyf1CIOiU#pYne})C|LvtOp z?`tm!D)HknLlc$pNc{j6Fnk=^OHg?~e3O%tgXQQjmb1lZk=|lZnaU&fiz3pvm#4N| znqsm732S0wV`HRJRsHYJczOhKC{nN|sI&%L1?-Tr*#++picC-mkQY>m=&X2g#!6zc zcMhlc7{|v*hr}{*Mye!kr8p!Ha#0N`;FkV+tJkaRYRA#Om7?hc6+>jZRWS|g;32A< zlq%5}q+0YrYT+O$RuV7Xkiuk|1Q*Qn+7Utz&$NYkvCQWFzpU_tFt3RMov(WGkO z!aL|)B_s3*!2b|r{2C${|26*kKNSD#xe&3;~s7M5GS`c+2Gcp$r)^WXO;qLxv0)GGxe*Awz}?88T$ZkkJhI MAHOr*RseVa0Fp7mZvX%Q literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore new file mode 100755 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.lock new file mode 100755 index 000000000..6fcc2ed9f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/Chart.yaml new file mode 100755 index 000000000..6a952e333 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt new file mode 100755 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE b/internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE new file mode 100755 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/README.md b/internal/chart/v3/loader/testdata/frobnitz_backslash/README.md new file mode 100755 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/_ignore_me new file mode 100755 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/Chart.yaml new file mode 100755 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/README.md new file mode 100755 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/Chart.yaml new file mode 100755 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast1/values.yaml new file mode 100755 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100755 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/templates/alpine-pod.yaml new file mode 100755 index 000000000..0ac5ca6a8 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml new file mode 100755 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_backslash/charts/mariner-4.3.2.tgz new file mode 100755 index 0000000000000000000000000000000000000000..5c6bc4dcb370466921bbde3c433614bed548313d GIT binary patch literal 910 zcmV;919AKxiwFP!000001MQe;XcIvgh9ly&p#D%nsMs-rcQ%{s1*uoOC|E1h3uzIj z>9pCpm%Ec{#S29gR7CN>`vMg~tXln{cu*A)Zxj_g@kdp#o~;@WbarD(Tah9*5pf;@ zJDZ)IZd$kcAQSzCpeT6& z*YaKYng3jWXeyJDWh;gr0%bg-Lk)$%k4eE4Avj{C+eWYNm?Vh@ttDbJPu;c%?p|mtzAg=Vku(IR2|N9^2Gx1HbS8ycFc9|EGf{ z`qwW^pS!MDTr%g+V>IXg0v~ksmt}z?djQd2l4a`uX(4lY`$VC2&8NiuM0kR z%`9J-dn5{G?Uol0Iltky)T+zUO;r!)Uzz^A-#~QzMdz&38UD5VdW^p-ZdtVI4V%AW z@czneV;@q_uD zl)9>uC+d1mIJ0Mvzxp5(TfabAdpY~zC$K&KG~W6Cy-n3qz%fZ5@nK77*sa>(t8Uj- zZk*P(X6}x?hYrDti(_V1^g88RGw|q|U3E8Sy)F1syX5iM+^@qbGPHsx?aTw;@}`u0 zy_1(w`z`BoJoD1#s;J%T{Z;uzY4}{EblyItW^2a>WyTG?@n|f3^sJkA{KpQSe(6`@ z?~q*o7?JH3lD#|yTfp!8zrYhqp#Gl*e%7B{A}DI8MaW{Y*%IkMbN){ffF}COd{CzT zpALG1iBZJ{?3Y|3Py7mq9g#?9?0pG@t*AP5oaC(C@#r&>M_G#W1E~keC5( zW + + Example icon + + + diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_backslash/ignore/me.txt new file mode 100755 index 000000000..e69de29bb diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl new file mode 100755 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_backslash/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/loader/testdata/frobnitz_backslash/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_backslash/values.yaml new file mode 100755 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_bom.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7f0edc6b2b87a1c9d815dbca7f55e56532f00d0f GIT binary patch literal 3453 zcmV-@4TAC?iwFP!000001MOT3coRh)4^-g{@_rl$2SecFA?-GsC#`KEv;`_HukwD_ zZnBeP>1H?VZrX;Y&;t=TDdHz6LP4b<50&o_J)vLgot!7=iBJU;MYLEzuvX!eN86j- zBqdF$u{EZM`}?4~nVFs0-T(al`~S~>W~>}*W+-7^Mh+#|Gt8_bS`l)n5sZ4hc*Ko* zwg1rv1+G_X4SJ16Z7?X*xK5|hDS$row^AQ2ULZIC6b>uFQ4G;wxnTLfQ&{WCAG1T+ zLD?9VgCS}|6tF?3YgqocHbDLcJ<0~^5Vila_=C&8;y|%S*3ru5z7-kGfcpGTv=f|w z(X1saRBZ&~zgDdaQrz@t<%~ zQ+#a9)+#+*_4NxS33&@gIRzhl1oi)n`at_ngOX(Z9|j=(U#rv^WKp5FLDNVzTU-vv2$-aYEi{GBk(O`+ zlH#2-Itu6-Oy>LVANBd4nv$42Ha$6nZ3N}NHgNn;uhVE`{)YpI|6YLuL~IMRvor}g znc9C8)aU=iWK&XFGUgyd)qV^!aE?KU=_S-vi0E$CS!` z{lH|Nu)*6E!a#}u>;#WoE#MLhdCDijY)G?C(UMRqN3t9sJqeIQmWGv$1U!VultBMk zXA~{w`FR?$4hjeA^WWpkg)$7-`1qgRh^yuJUpUa9{4+dZp$x~bj{*kM|2U2Z$lqwx z7-jt*4*2{(-p;yc5||bd6Sr%klrPpdtHT5_nzn-{hH) zCG`mD13SiLh8%;w(){gBf0gR4`ApumXcYgwHdtd7K+yOfZV1r-qAHO29}YFPj+A3dS5#mlg&lb-CUyMa_2&$UPezol z(I=cQIQ8)x`QNVIv+h2{@Xc^<%kEPLRn4z@;+uzeMULxV_{MwFD)Q#uw>cr}^y$ac zvtR39zH9&BvMq<6U2tJ1_IAzUnC+h&{$AIMJ0AOw^Ha+nP?jZIBD-qFuIY~T1d|89 zePE`!{m}BoYS*|$J6nJ8NcED9H){sJe{J!yswF=h?ru4AzB_u*ny>OoGZgnX^NYVj z5RCt~#&nwAMicx$T;#v(|NZv(> z@hquyK!G3$fe_c8c)Y^-^RbDLh6E3>u_$5T!YGvD85QvSMkQ&&3~6b-3IK%DiDlJ# zi5SHqLxLe}kc=|B>(rJGxopbfso-{()ixi{7y!PwlGJUH&lynW)(v`}ap6Lhl6OLj zc!L}ig)fX}Y04tqk8c!NH5MX|Rs}F`i43PyL~7DI>>RLODTkm0?+O~qfu0cEiH9(; zS}ex1h{ijF9t&owqRkW&&C?J%RgtO~L;=<8X0m;Q@xO^KUmXPFzgib)|KVDl%>S@J z^8dsDix`g)yRvuuN9AxwdAfk?mEFx~kpJE_fl<;bz{dN0{s_W<+~6JmF&cGhao|U- zQR{WG{}&b@|Ic$0ad`t}?`-P6yx*6vZK)n_+Hn$Jnq$~_>E4&79vn1nNNF3Yt zPQAJQz`8ky+9Y?GlvG)Aqh(jq?ip2EbH{h>$ad)(S2=3(tDE;c&|b*4JUnx)%FuCc z@0Jg3+_Z_^kn{DXs*!n5Ki}_2MM~dZrf$jQ59BAmd{+PXK)@aB-zx99{_Z!1=a=8x zzC-${m-^#96Wb`(Ou4lBo|~}f>&-PU*oR%|(_>jT>yfR6`xo3lDl?_SqL+sC8GZ2s z{&~>3YT1@SZpDf3XJ31!X4Yc+gX`A9A)j z?=W|Oa*!cXZJ4qm@-GCbh3^iy=V@DBYN>NZ<-C-L0moM5_nwgd7qjBKB}s}M-A2$o z_NP@xl#D1OhMau$a3WZ_mwN2xB3;ipg)2|4*m`XpU)pw4?zV(2>n#zH<*@pHL#ub` zdVg9Ffa@osQwt8fJZ+Yy`~!=wyryLz;p%$0q~CnShZ#@8i#1eV z-hCr!tLyzeqqf;hKN?zr z{`j}DE7P)btCwx+(WjtSK_z24kyV*@ZPQb4?p!zU;=Hj#|8C1Kz79IiEFM06e$0)t z17D4=)_s1hx+v|V^b$O!PbD$Nesa$zXLm6tCNs}{yx;om6Ge};du!z?i*?GC*mE`DjPc0}~w|>HnsBgHqAC7&!bH``j zR878?O#L+LMd9;=ZPu)JTEl<0EvmFfF1WILW6sTY+HGERVd|F^`Lk-euf5#u$)dP> zXO>jlC@$$dW=nNJt3@9d%u)@Svm^OG=DnvnT^P<+kL&d4fa^yVj4!M%Xg4qK@d<}o zm9;h{p1aWcYK@7jT5r-Xzf{^wp)06al~$E;|Iv!#eMft)9&t!rXwKizg0T+EC9{Vj zMVNc~{MN|hNBb5of9Kp!quaJMuUrWLWz4hU=tsD1R64MtxSUTo(r?2jL ze8I$VuZE*r6R{Gcwr!#4a(ixwx#G8KcGga7sQKgn7`I{Uv#^Z&JaIsP9OG$8+w zE&>Z`|Lg1R|6|a~>%YPR|MlPgD?p_nkZ&2dw1vgpGv`UhE!5|~xUmVwXWK&8Mlk;C z4TeDe<7#>TudpC)SgwQiZTb?g68Df9mY|G7@&_n`;bYNRyvn=(O-@b@mZQa3&K9jf za*IZJDo?SWEh3G6d1}k0DJJW7h4Gk}m}n_i<-ZM$=Z-)QMGE$Kl}2w+0Xw8@cES6D zBI8v8UHW{wHwg6 zm7?i*6+>jZRnc|p;32A{XZJg zG%Lo|#E2PKP`i{umBVs0sam-34*Fcl2r=sOKNJzax(LR9JTU%;{C~AP{vQ@d@jp}T z%+EUOs?dp*vbi{~`|pbkirZ`gQCFyxN@NI2lf!Fg2&g0_B5e%d&6DGYGGxe*Awz}? f88T$ZkRd~c3>h+H$dDmJM!4~R>e)Ct0C)fZ$tl;` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore new file mode 100644 index 000000000..7a4b92da2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.lock new file mode 100644 index 000000000..ed43b227f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/Chart.yaml new file mode 100644 index 000000000..924fae6fc --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt new file mode 100644 index 000000000..77c4e724a --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE b/internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE new file mode 100644 index 000000000..c27b00bf2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_bom/README.md new file mode 100644 index 000000000..e9c40031b --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/_ignore_me new file mode 100644 index 000000000..a7e3a38b7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/Chart.yaml new file mode 100644 index 000000000..6fe4f411f --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/README.md new file mode 100644 index 000000000..ea7526bee --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..0732c7d7d --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..f690d53c4 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..f3e662a28 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml new file mode 100644 index 000000000..6b7cb2596 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_bom/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5c6bc4dcb370466921bbde3c433614bed548313d GIT binary patch literal 910 zcmV;919AKxiwFP!000001MQe;XcIvgh9ly&p#D%nsMs-rcQ%{s1*uoOC|E1h3uzIj z>9pCpm%Ec{#S29gR7CN>`vMg~tXln{cu*A)Zxj_g@kdp#o~;@WbarD(Tah9*5pf;@ zJDZ)IZd$kcAQSzCpeT6& z*YaKYng3jWXeyJDWh;gr0%bg-Lk)$%k4eE4Avj{C+eWYNm?Vh@ttDbJPu;c%?p|mtzAg=Vku(IR2|N9^2Gx1HbS8ycFc9|EGf{ z`qwW^pS!MDTr%g+V>IXg0v~ksmt}z?djQd2l4a`uX(4lY`$VC2&8NiuM0kR z%`9J-dn5{G?Uol0Iltky)T+zUO;r!)Uzz^A-#~QzMdz&38UD5VdW^p-ZdtVI4V%AW z@czneV;@q_uD zl)9>uC+d1mIJ0Mvzxp5(TfabAdpY~zC$K&KG~W6Cy-n3qz%fZ5@nK77*sa>(t8Uj- zZk*P(X6}x?hYrDti(_V1^g88RGw|q|U3E8Sy)F1syX5iM+^@qbGPHsx?aTw;@}`u0 zy_1(w`z`BoJoD1#s;J%T{Z;uzY4}{EblyItW^2a>WyTG?@n|f3^sJkA{KpQSe(6`@ z?~q*o7?JH3lD#|yTfp!8zrYhqp#Gl*e%7B{A}DI8MaW{Y*%IkMbN){ffF}COd{CzT zpALG1iBZJ{?3Y|3Py7mq9g#?9?0pG@t*AP5oaC(C@#r&>M_G#W1E~keC5( zW + + Example icon + + + diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_with_bom/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl new file mode 100644 index 000000000..bb29c5491 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_bom/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_bom/values.yaml new file mode 100644 index 000000000..c24ceadf9 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.lock new file mode 100644 index 000000000..6fcc2ed9f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/Chart.yaml new file mode 100644 index 000000000..1b63fc3e2 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..21ae20aad --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5c6bc4dcb370466921bbde3c433614bed548313d GIT binary patch literal 910 zcmV;919AKxiwFP!000001MQe;XcIvgh9ly&p#D%nsMs-rcQ%{s1*uoOC|E1h3uzIj z>9pCpm%Ec{#S29gR7CN>`vMg~tXln{cu*A)Zxj_g@kdp#o~;@WbarD(Tah9*5pf;@ zJDZ)IZd$kcAQSzCpeT6& z*YaKYng3jWXeyJDWh;gr0%bg-Lk)$%k4eE4Avj{C+eWYNm?Vh@ttDbJPu;c%?p|mtzAg=Vku(IR2|N9^2Gx1HbS8ycFc9|EGf{ z`qwW^pS!MDTr%g+V>IXg0v~ksmt}z?djQd2l4a`uX(4lY`$VC2&8NiuM0kR z%`9J-dn5{G?Uol0Iltky)T+zUO;r!)Uzz^A-#~QzMdz&38UD5VdW^p-ZdtVI4V%AW z@czneV;@q_uD zl)9>uC+d1mIJ0Mvzxp5(TfabAdpY~zC$K&KG~W6Cy-n3qz%fZ5@nK77*sa>(t8Uj- zZk*P(X6}x?hYrDti(_V1^g88RGw|q|U3E8Sy)F1syX5iM+^@qbGPHsx?aTw;@}`u0 zy_1(w`z`BoJoD1#s;J%T{Z;uzY4}{EblyItW^2a>WyTG?@n|f3^sJkA{KpQSe(6`@ z?~q*o7?JH3lD#|yTfp!8zrYhqp#Gl*e%7B{A}DI8MaW{Y*%IkMbN){ffF}COd{CzT zpALG1iBZJ{?3Y|3Py7mq9g#?9?0pG@t*AP5oaC(C@#r&>M_G#W1E~keC5( zW + + Example icon + + + diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null new file mode 120000 index 000000000..dc1dc0cde --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/null @@ -0,0 +1 @@ +/dev/null \ No newline at end of file diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_dev_null/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.lock new file mode 100644 index 000000000..6fcc2ed9f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/Chart.yaml new file mode 100644 index 000000000..1b63fc3e2 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..21ae20aad --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..5c6bc4dcb370466921bbde3c433614bed548313d GIT binary patch literal 910 zcmV;919AKxiwFP!000001MQe;XcIvgh9ly&p#D%nsMs-rcQ%{s1*uoOC|E1h3uzIj z>9pCpm%Ec{#S29gR7CN>`vMg~tXln{cu*A)Zxj_g@kdp#o~;@WbarD(Tah9*5pf;@ zJDZ)IZd$kcAQSzCpeT6& z*YaKYng3jWXeyJDWh;gr0%bg-Lk)$%k4eE4Avj{C+eWYNm?Vh@ttDbJPu;c%?p|mtzAg=Vku(IR2|N9^2Gx1HbS8ycFc9|EGf{ z`qwW^pS!MDTr%g+V>IXg0v~ksmt}z?djQd2l4a`uX(4lY`$VC2&8NiuM0kR z%`9J-dn5{G?Uol0Iltky)T+zUO;r!)Uzz^A-#~QzMdz&38UD5VdW^p-ZdtVI4V%AW z@czneV;@q_uD zl)9>uC+d1mIJ0Mvzxp5(TfabAdpY~zC$K&KG~W6Cy-n3qz%fZ5@nK77*sa>(t8Uj- zZk*P(X6}x?hYrDti(_V1^g88RGw|q|U3E8Sy)F1syX5iM+^@qbGPHsx?aTw;@}`u0 zy_1(w`z`BoJoD1#s;J%T{Z;uzY4}{EblyItW^2a>WyTG?@n|f3^sJkA{KpQSe(6`@ z?~q*o7?JH3lD#|yTfp!8zrYhqp#Gl*e%7B{A}DI8MaW{Y*%IkMbN){ffF}COd{CzT zpALG1iBZJ{?3Y|3Py7mq9g#?9?0pG@t*AP5oaC(C@#r&>M_G#W1E~keC5( zW + + Example icon + + + diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml b/internal/chart/v3/loader/testdata/frobnitz_with_symlink/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/loader/testdata/genfrob.sh b/internal/chart/v3/loader/testdata/genfrob.sh new file mode 100755 index 000000000..eae68906b --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/mariner/Chart.yaml b/internal/chart/v3/loader/testdata/mariner/Chart.yaml new file mode 100644 index 000000000..4d3eea730 --- /dev/null +++ b/internal/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/internal/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz b/internal/chart/v3/loader/testdata/mariner/charts/albatross-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..ec7bfbfcf38602dc9f8608c0ac171d5392c16342 GIT binary patch literal 282 zcmb2|=3oE==C@bwXB}3MVEqvDTSUIH!EbYhLiu9zX4|9FubD9lx6e9sukLKZOAYPR z3vc+X_e<+fSs~+p!*otl|3|&(9-CEL=DCGt{w%DSuBrRJSC1>xM=dl_YwEW(jz{_$v6_UC$$^FP-|^JRWLw0HMcx!?oKbo8IC=PI36pE>1>$Gu+)^M5}7 z$$#hetY_g0+syKuzR$6;zw*VQ|Jv6rGvnCa)+|`Qabx0!lWv6@6!+;)(%w30(bvo6 zpJM)eX?@`B_t$=dbZ*Qe_JXzlgTmAQUJsJ~^56Q*Px;fc4`mc9emiIWCd=_ia#rn% e%(ruS@0R6X%!7CyMmF4kA*$s6gOfpnfdK$0bcT`u literal 0 HcmV?d00001 diff --git a/internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl b/internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl new file mode 100644 index 000000000..29c11843a --- /dev/null +++ b/internal/chart/v3/loader/testdata/mariner/templates/placeholder.tpl @@ -0,0 +1 @@ +# This is a placeholder. diff --git a/internal/chart/v3/loader/testdata/mariner/values.yaml b/internal/chart/v3/loader/testdata/mariner/values.yaml new file mode 100644 index 000000000..b0ccb0086 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/metadata.go b/internal/chart/v3/metadata.go new file mode 100644 index 000000000..4629d571b --- /dev/null +++ b/internal/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/internal/chart/v3/metadata_test.go b/internal/chart/v3/metadata_test.go new file mode 100644 index 000000000..596a03695 --- /dev/null +++ b/internal/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/internal/chart/v3/util/capabilities.go b/internal/chart/v3/util/capabilities.go new file mode 100644 index 000000000..23b6d46fa --- /dev/null +++ b/internal/chart/v3/util/capabilities.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 util + +import ( + "fmt" + "slices" + "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 "helm.sh/helm/v4/internal/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 { + 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 +} diff --git a/internal/chart/v3/util/capabilities_test.go b/internal/chart/v3/util/capabilities_test.go new file mode 100644 index 000000000..aa9be9db8 --- /dev/null +++ b/internal/chart/v3/util/capabilities_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 util + +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 != "v4.0" { + t.Errorf("Expected default HelmVersion to be v4.0, 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/internal/chart/v3/util/chartfile.go b/internal/chart/v3/util/chartfile.go new file mode 100644 index 000000000..25271e1cf --- /dev/null +++ b/internal/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 "helm.sh/helm/v4/internal/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/internal/chart/v3/util/chartfile_test.go b/internal/chart/v3/util/chartfile_test.go new file mode 100644 index 000000000..c3d19c381 --- /dev/null +++ b/internal/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 "helm.sh/helm/v4/internal/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/internal/chart/v3/util/coalesce.go b/internal/chart/v3/util/coalesce.go new file mode 100644 index 000000000..caea2e119 --- /dev/null +++ b/internal/chart/v3/util/coalesce.go @@ -0,0 +1,308 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "maps" + + "github.com/mitchellh/copystructure" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +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) { + 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) { + 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, fmt.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)) + 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.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 { + // 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.Chart, key string, merge bool) bool { + for _, subchart := range chrt.Dependencies() { + if subchart.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 + } + 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 { + 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 +} diff --git a/internal/chart/v3/util/coalesce_test.go b/internal/chart/v3/util/coalesce_test.go new file mode 100644 index 000000000..4770b601d --- /dev/null +++ b/internal/chart/v3/util/coalesce_test.go @@ -0,0 +1,723 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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 ( + "encoding/json" + "fmt" + "maps" + "testing" + + "github.com/stretchr/testify/assert" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +// ref: http://www.yaml.org/spec/1.2/spec.html#id2803362 +var testCoalesceValuesYaml = []byte(` +top: yup +bottom: null +right: Null +left: NULL +front: ~ +back: "" +nested: + boat: null + +global: + name: Ishmael + subject: Queequeg + nested: + 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 + boat: null + object: null +`) + +func withDeps(c *chart.Chart, deps ...*chart.Chart) *chart.Chart { + c.AddDependency(deps...) + return c +} + +func TestCoalesceValues(t *testing.T) { + is := assert.New(t) + + c := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "moby"}, + Values: map[string]interface{}{ + "back": "exists", + "bottom": "exists", + "front": "exists", + "left": "exists", + "name": "moby", + "nested": map[string]interface{}{"boat": true}, + "override": "bad", + "right": "exists", + "scope": "moby", + "top": "nope", + "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{ + Metadata: &chart.Metadata{Name: "pequod"}, + Values: map[string]interface{}{ + "name": "pequod", + "scope": "pequod", + "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", "foo2": "bar2"}, + "nested2": map[string]interface{}{"l2": "ahab"}, + }, + "scope": "ahab", + "name": "ahab", + "boat": true, + "nested": map[string]interface{}{"foo": false, "boat": true}, + "object": map[string]interface{}{"foo": "bar"}, + }, + }, + ), + &chart.Chart{ + Metadata: &chart.Metadata{Name: "spouter"}, + Values: map[string]interface{}{ + "scope": "spouter", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "spouter"}, + }, + }, + }, + ) + + vals, err := ReadValues(testCoalesceValuesYaml) + if err != nil { + t.Fatal(err) + } + + // 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)) + maps.Copy(valsCopy, vals) + + v, err := CoalesceValues(c, vals) + if err != nil { + t.Fatal(err) + } + j, _ := json.MarshalIndent(v, "", " ") + t.Logf("Coalesced Values: %s", string(j)) + + tests := []struct { + tpl string + expect string + }{ + {"{{.top}}", "yup"}, + {"{{.back}}", ""}, + {"{{.name}}", "moby"}, + {"{{.global.name}}", "Ishmael"}, + {"{{.global.subject}}", "Queequeg"}, + {"{{.global.harpooner}}", ""}, + {"{{.pequod.name}}", "pequod"}, + {"{{.pequod.ahab.name}}", "ahab"}, + {"{{.pequod.ahab.scope}}", "whale"}, + {"{{.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"}, + {"{{.pequod.global.nested.foo}}", ""}, + {"{{.pequod.global.subject}}", "Queequeg"}, + {"{{.spouter.global.name}}", "Ishmael"}, + {"{{.spouter.global.harpooner}}", ""}, + + {"{{.global.nested.boat}}", "true"}, + {"{{.pequod.global.nested.boat}}", "true"}, + {"{{.spouter.global.nested.boat}}", "true"}, + {"{{.pequod.global.nested.sail}}", "true"}, + {"{{.spouter.global.nested.sail}}", ""}, + + {"{{.global.nested2.l0}}", "moby"}, + {"{{.global.nested2.l1}}", ""}, + {"{{.global.nested2.l2}}", ""}, + {"{{.pequod.global.nested2.l0}}", "moby"}, + {"{{.pequod.global.nested2.l1}}", "pequod"}, + {"{{.pequod.global.nested2.l2}}", ""}, + {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, + {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, + {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, + {"{{.spouter.global.nested2.l0}}", "moby"}, + {"{{.spouter.global.nested2.l1}}", "spouter"}, + {"{{.spouter.global.nested2.l2}}", ""}, + } + + for _, tt := range tests { + if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { + t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) + } + } + + nullKeys := []string{"bottom", "right", "left", "front"} + for _, nullKey := range nullKeys { + if _, ok := v[nullKey]; ok { + t.Errorf("Expected key %q to be removed, still present", nullKey) + } + } + + if _, ok := v["nested"].(map[string]interface{})["boat"]; ok { + t.Error("Expected nested boat key to be removed, still present") + } + + subchart := v["pequod"].(map[string]interface{}) + if _, ok := subchart["boat"]; ok { + t.Error("Expected subchart boat 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 TestMergeValues(t *testing.T) { + is := assert.New(t) + + c := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "moby"}, + Values: map[string]interface{}{ + "back": "exists", + "bottom": "exists", + "front": "exists", + "left": "exists", + "name": "moby", + "nested": map[string]interface{}{"boat": true}, + "override": "bad", + "right": "exists", + "scope": "moby", + "top": "nope", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l0": "moby"}, + }, + }, + }, + withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "pequod"}, + Values: map[string]interface{}{ + "name": "pequod", + "scope": "pequod", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "pequod"}, + }, + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{Name: "ahab"}, + Values: map[string]interface{}{ + "global": map[string]interface{}{ + "nested": map[string]interface{}{"foo": "bar"}, + "nested2": map[string]interface{}{"l2": "ahab"}, + }, + "scope": "ahab", + "name": "ahab", + "boat": true, + "nested": map[string]interface{}{"foo": false, "bar": true}, + }, + }, + ), + &chart.Chart{ + Metadata: &chart.Metadata{Name: "spouter"}, + Values: map[string]interface{}{ + "scope": "spouter", + "global": map[string]interface{}{ + "nested2": map[string]interface{}{"l1": "spouter"}, + }, + }, + }, + ) + + vals, err := ReadValues(testCoalesceValuesYaml) + if err != nil { + t.Fatal(err) + } + + // 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)) + maps.Copy(valsCopy, vals) + + v, err := MergeValues(c, vals) + if err != nil { + t.Fatal(err) + } + j, _ := json.MarshalIndent(v, "", " ") + t.Logf("Coalesced Values: %s", string(j)) + + tests := []struct { + tpl string + expect string + }{ + {"{{.top}}", "yup"}, + {"{{.back}}", ""}, + {"{{.name}}", "moby"}, + {"{{.global.name}}", "Ishmael"}, + {"{{.global.subject}}", "Queequeg"}, + {"{{.global.harpooner}}", ""}, + {"{{.pequod.name}}", "pequod"}, + {"{{.pequod.ahab.name}}", "ahab"}, + {"{{.pequod.ahab.scope}}", "whale"}, + {"{{.pequod.ahab.nested.foo}}", "true"}, + {"{{.pequod.ahab.global.name}}", "Ishmael"}, + {"{{.pequod.ahab.global.nested.foo}}", "bar"}, + {"{{.pequod.ahab.global.subject}}", "Queequeg"}, + {"{{.pequod.ahab.global.harpooner}}", "Tashtego"}, + {"{{.pequod.global.name}}", "Ishmael"}, + {"{{.pequod.global.nested.foo}}", ""}, + {"{{.pequod.global.subject}}", "Queequeg"}, + {"{{.spouter.global.name}}", "Ishmael"}, + {"{{.spouter.global.harpooner}}", ""}, + + {"{{.global.nested.boat}}", "true"}, + {"{{.pequod.global.nested.boat}}", "true"}, + {"{{.spouter.global.nested.boat}}", "true"}, + {"{{.pequod.global.nested.sail}}", "true"}, + {"{{.spouter.global.nested.sail}}", ""}, + + {"{{.global.nested2.l0}}", "moby"}, + {"{{.global.nested2.l1}}", ""}, + {"{{.global.nested2.l2}}", ""}, + {"{{.pequod.global.nested2.l0}}", "moby"}, + {"{{.pequod.global.nested2.l1}}", "pequod"}, + {"{{.pequod.global.nested2.l2}}", ""}, + {"{{.pequod.ahab.global.nested2.l0}}", "moby"}, + {"{{.pequod.ahab.global.nested2.l1}}", "pequod"}, + {"{{.pequod.ahab.global.nested2.l2}}", "ahab"}, + {"{{.spouter.global.nested2.l0}}", "moby"}, + {"{{.spouter.global.nested2.l1}}", "spouter"}, + {"{{.spouter.global.nested2.l2}}", ""}, + } + + for _, tt := range tests { + if o, err := ttpl(tt.tpl, v); err != nil || o != tt.expect { + t.Errorf("Expected %q to expand to %q, got %q", tt.tpl, tt.expect, o) + } + } + + // nullKeys is different from coalescing. Here the null/nil values are not + // removed. + nullKeys := []string{"bottom", "right", "left", "front"} + for _, nullKey := range nullKeys { + if vv, ok := v[nullKey]; !ok { + t.Errorf("Expected key %q to be present but it was removed", nullKey) + } else if vv != nil { + t.Errorf("Expected key %q to be null but it has a value of %v", nullKey, vv) + } + } + + if _, ok := v["nested"].(map[string]interface{})["boat"]; !ok { + t.Error("Expected nested boat key to be present but it was removed") + } + + subchart := v["pequod"].(map[string]interface{})["ahab"].(map[string]interface{}) + if _, ok := subchart["boat"]; !ok { + t.Error("Expected subchart boat key to be present but it was removed") + } + + if _, ok := subchart["nested"].(map[string]interface{})["bar"]; !ok { + t.Error("Expected subchart nested bar key to be present but it was removed") + } + + // CoalesceValues should not mutate the passed arguments + is.Equal(valsCopy, vals) +} + +func TestCoalesceTables(t *testing.T) { + dst := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": nil, + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": nil, + } + src := map[string]interface{}{ + "occupation": "whaler", + "address": map[string]interface{}{ + "state": "MA", + "street": "234 Spouter Inn Ct.", + "country": "US", + }, + "details": "empty", + "boat": map[string]interface{}{ + "mast": true, + }, + "hole": "black", + } + + // What we expect is that anything in dst overrides anything in src, but that + // otherwise the values are coalesced. + CoalesceTables(dst, src) + + if dst["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst["name"]) + } + if dst["occupation"] != "whaler" { + t.Errorf("Unexpected occupation: %s", dst["occupation"]) + } + + addr, ok := dst["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr["street"]) + } + + if addr["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr["city"]) + } + + if addr["state"].(string) != "MA" { + t.Errorf("Unexpected state: %v", addr["state"]) + } + + if _, ok = addr["country"]; ok { + t.Error("The country is not left out.") + } + + if det, ok := dst["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst["details"]) + } else if _, ok := det["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst["boat"]) + } + + if _, ok = dst["hole"]; ok { + t.Error("The hole still exists.") + } + + dst2 := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": "US", + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": "black", + } + + // What we expect is that anything in dst should have all values set, + // this happens when the --reuse-values flag is set but the chart has no modifications yet + CoalesceTables(dst2, nil) + + if dst2["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst2["name"]) + } + + addr2, ok := dst2["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr2["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr2["street"]) + } + + if addr2["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr2["city"]) + } + + if addr2["country"].(string) != "US" { + t.Errorf("Unexpected Country: %v", addr2["country"]) + } + + if det2, ok := dst2["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst2["details"]) + } else if _, ok := det2["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst2["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst2["boat"]) + } + + if dst2["hole"].(string) != "black" { + t.Errorf("Expected hole string, got %v", dst2["boat"]) + } +} + +func TestMergeTables(t *testing.T) { + dst := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": nil, + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": nil, + } + src := map[string]interface{}{ + "occupation": "whaler", + "address": map[string]interface{}{ + "state": "MA", + "street": "234 Spouter Inn Ct.", + "country": "US", + }, + "details": "empty", + "boat": map[string]interface{}{ + "mast": true, + }, + "hole": "black", + } + + // What we expect is that anything in dst overrides anything in src, but that + // otherwise the values are coalesced. + MergeTables(dst, src) + + if dst["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst["name"]) + } + if dst["occupation"] != "whaler" { + t.Errorf("Unexpected occupation: %s", dst["occupation"]) + } + + addr, ok := dst["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr["street"]) + } + + if addr["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr["city"]) + } + + if addr["state"].(string) != "MA" { + t.Errorf("Unexpected state: %v", addr["state"]) + } + + // This is one test that is different from CoalesceTables. Because country + // is a nil value and it's not removed it's still present. + if _, ok = addr["country"]; !ok { + t.Error("The country is left out.") + } + + if det, ok := dst["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst["details"]) + } else if _, ok := det["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst["boat"]) + } + + // This is one test that is different from CoalesceTables. Because hole + // is a nil value and it's not removed it's still present. + if _, ok = dst["hole"]; !ok { + t.Error("The hole no longer exists.") + } + + dst2 := map[string]interface{}{ + "name": "Ishmael", + "address": map[string]interface{}{ + "street": "123 Spouter Inn Ct.", + "city": "Nantucket", + "country": "US", + }, + "details": map[string]interface{}{ + "friends": []string{"Tashtego"}, + }, + "boat": "pequod", + "hole": "black", + "nilval": nil, + } + + // What we expect is that anything in dst should have all values set, + // this happens when the --reuse-values flag is set but the chart has no modifications yet + MergeTables(dst2, nil) + + if dst2["name"] != "Ishmael" { + t.Errorf("Unexpected name: %s", dst2["name"]) + } + + addr2, ok := dst2["address"].(map[string]interface{}) + if !ok { + t.Fatal("Address went away.") + } + + if addr2["street"].(string) != "123 Spouter Inn Ct." { + t.Errorf("Unexpected address: %v", addr2["street"]) + } + + if addr2["city"].(string) != "Nantucket" { + t.Errorf("Unexpected city: %v", addr2["city"]) + } + + if addr2["country"].(string) != "US" { + t.Errorf("Unexpected Country: %v", addr2["country"]) + } + + if det2, ok := dst2["details"].(map[string]interface{}); !ok { + t.Fatalf("Details is the wrong type: %v", dst2["details"]) + } else if _, ok := det2["friends"]; !ok { + t.Error("Could not find your friends. Maybe you don't have any. :-(") + } + + if dst2["boat"].(string) != "pequod" { + t.Errorf("Expected boat string, got %v", dst2["boat"]) + } + + if dst2["hole"].(string) != "black" { + t.Errorf("Expected hole string, got %v", dst2["boat"]) + } + + if dst2["nilval"] != nil { + t.Error("Expected nilvalue to have nil value but it does not") + } +} + +func TestCoalesceValuesWarnings(t *testing.T) { + + c := withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "level1"}, + Values: map[string]interface{}{ + "name": "moby", + }, + }, + withDeps(&chart.Chart{ + Metadata: &chart.Metadata{Name: "level2"}, + Values: map[string]interface{}{ + "name": "pequod", + }, + }, + &chart.Chart{ + Metadata: &chart.Metadata{Name: "level3"}, + Values: map[string]interface{}{ + "name": "ahab", + "boat": true, + "spear": map[string]interface{}{ + "tip": true, + "sail": map[string]interface{}{ + "cotton": true, + }, + }, + }, + }, + ), + ) + + vals := map[string]interface{}{ + "level2": map[string]interface{}{ + "level3": map[string]interface{}{ + "boat": map[string]interface{}{"mast": true}, + "spear": map[string]interface{}{ + "tip": map[string]interface{}{ + "sharp": true, + }, + "sail": true, + }, + }, + }, + } + + warnings := make([]string, 0) + printf := func(format string, v ...interface{}) { + t.Logf(format, v...) + warnings = append(warnings, fmt.Sprintf(format, v...)) + } + + _, err := coalesce(printf, c, vals, "", false) + if err != nil { + t.Fatal(err) + } + + t.Logf("vals: %v", vals) + assert.Contains(t, warnings, "warning: skipped value for level1.level2.level3.boat: Not a table.") + assert.Contains(t, warnings, "warning: destination for level1.level2.level3.spear.tip is a table. Ignoring non-table value (true)") + assert.Contains(t, warnings, "warning: cannot overwrite table with non table for level1.level2.level3.spear.sail (map[cotton:true])") + +} + +func TestConcatPrefix(t *testing.T) { + assert.Equal(t, "b", concatPrefix("", "b")) + assert.Equal(t, "a.b", concatPrefix("a", "b")) +} diff --git a/internal/chart/v3/util/compatible.go b/internal/chart/v3/util/compatible.go new file mode 100644 index 000000000..d384d2d45 --- /dev/null +++ b/internal/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/internal/chart/v3/util/compatible_test.go b/internal/chart/v3/util/compatible_test.go new file mode 100644 index 000000000..e17d33e35 --- /dev/null +++ b/internal/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/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go new file mode 100644 index 000000000..72fed5955 --- /dev/null +++ b/internal/chart/v3/util/create.go @@ -0,0 +1,832 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "io" + "os" + "path/filepath" + "regexp" + "strings" + + "sigs.k8s.io/yaml" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/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: 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: {} + # 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(src) + if err != nil { + return fmt.Errorf("could not load %s: %w", src, err) + } + + 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 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: []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), + }, + { + // 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/internal/chart/v3/util/create_test.go b/internal/chart/v3/util/create_test.go new file mode 100644 index 000000000..b3b58cc5a --- /dev/null +++ b/internal/chart/v3/util/create_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 util + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/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(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(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/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go new file mode 100644 index 000000000..bd5032ce4 --- /dev/null +++ b/internal/chart/v3/util/dependencies.go @@ -0,0 +1,366 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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 ( + "log/slog" + "strings" + + "github.com/mitchellh/copystructure" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +// ProcessDependencies checks through this chart's dependencies, processing accordingly. +func ProcessDependencies(c *chart.Chart, v 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 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.(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 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 := 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 { + slog.Warn("ImportValues missing table from chart", "chart", r.Name, slog.Any("error", 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 { + slog.Warn("ImportValues missing table", slog.Any("error", 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) +} diff --git a/internal/chart/v3/util/dependencies_test.go b/internal/chart/v3/util/dependencies_test.go new file mode 100644 index 000000000..55839fe65 --- /dev/null +++ b/internal/chart/v3/util/dependencies_test.go @@ -0,0 +1,569 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "sort" + "strconv" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/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) + } + } + }) + } +} + +// 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 := 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 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 := 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 := 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/internal/chart/v3/util/doc.go b/internal/chart/v3/util/doc.go new file mode 100644 index 000000000..002d5babc --- /dev/null +++ b/internal/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 'helm.sh/helm/pkg/chart' +package directly. +*/ +package util // import chartutil "helm.sh/helm/v4/internal/chart/v3/util" diff --git a/internal/chart/v3/util/errors.go b/internal/chart/v3/util/errors.go new file mode 100644 index 000000000..a175b9758 --- /dev/null +++ b/internal/chart/v3/util/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 util + +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/internal/chart/v3/util/errors_test.go b/internal/chart/v3/util/errors_test.go new file mode 100644 index 000000000..b8ae86384 --- /dev/null +++ b/internal/chart/v3/util/errors_test.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 util + +import ( + "testing" +) + +func TestErrorNoTableDoesNotPanic(t *testing.T) { + x := "empty" + + y := ErrNoTable{x} + + t.Logf("error is: %s", y) +} + +func TestErrorNoValueDoesNotPanic(t *testing.T) { + x := "empty" + + y := ErrNoValue{x} + + t.Logf("error is: %s", y) +} diff --git a/internal/chart/v3/util/expand.go b/internal/chart/v3/util/expand.go new file mode 100644 index 000000000..6cbbeabf2 --- /dev/null +++ b/internal/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 "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/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 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/internal/chart/v3/util/expand_test.go b/internal/chart/v3/util/expand_test.go new file mode 100644 index 000000000..280995f7e --- /dev/null +++ b/internal/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/internal/chart/v3/util/jsonschema.go b/internal/chart/v3/util/jsonschema.go new file mode 100644 index 000000000..9fe35904e --- /dev/null +++ b/internal/chart/v3/util/jsonschema.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 util + +import ( + "bytes" + "errors" + "fmt" + "log/slog" + "strings" + + "github.com/santhosh-tekuri/jsonschema/v6" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +// 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 { + slog.Debug("chart name", "chart-name", chrt.Name()) + err := ValidateAgainstSingleSchema(values, chrt.Schema) + if err != nil { + sb.WriteString(fmt.Sprintf("%s:\n", chrt.Name())) + sb.WriteString(err.Error()) + } + } + slog.Debug("number of dependencies in the chart", "dependencies", len(chrt.Dependencies())) + // 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) + } + }() + + // 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) + + compiler := jsonschema.NewCompiler() + 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 +} + +// 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/internal/chart/v3/util/jsonschema_test.go b/internal/chart/v3/util/jsonschema_test.go new file mode 100644 index 000000000..0a3820377 --- /dev/null +++ b/internal/chart/v3/util/jsonschema_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 util + +import ( + "os" + "testing" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +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 := `"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 := 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) + } +} diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go new file mode 100644 index 000000000..3125cc3c9 --- /dev/null +++ b/internal/chart/v3/util/save.go @@ -0,0 +1,253 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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 "helm.sh/helm/v4/internal/chart/v3" +) + +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 [][]*chart.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); 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); 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 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/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go new file mode 100644 index 000000000..852675bb0 --- /dev/null +++ b/internal/chart/v3/util/save_test.go @@ -0,0 +1,261 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "io" + "os" + "path" + "path/filepath" + "regexp" + "strings" + "testing" + "time" + + chart "helm.sh/helm/v4/internal/chart/v3" + "helm.sh/helm/v4/internal/chart/v3/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.APIVersionV3, + 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 !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(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: []*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.APIVersionV3, + 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.APIVersionV3, + 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/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/Chart.yaml new file mode 100644 index 000000000..4a4da7996 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/Chart.yaml new file mode 100644 index 000000000..0f3afd8c6 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/Chart.yaml new file mode 100644 index 000000000..3e0bf725b --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/charts/grandchild/templates/dummy.yaml new file mode 100644 index 000000000..1830492ef --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/charts/child/templates/dummy.yaml new file mode 100644 index 000000000..b5d55af7c --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml b/internal/chart/v3/util/testdata/chart-with-dependency-aliased-twice/values.yaml new file mode 100644 index 000000000..695521a4a --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/Chart.yaml new file mode 100644 index 000000000..f2f0610b5 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/Chart.yaml new file mode 100644 index 000000000..08ccac9e5 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/Chart.yaml new file mode 100644 index 000000000..3e0bf725b --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/charts/grandchild/values.yaml new file mode 100644 index 000000000..f51c594f4 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/charts/child/templates/dummy.yaml new file mode 100644 index 000000000..3140f53dd --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml b/internal/chart/v3/util/testdata/chart-with-import-from-aliased-dependencies/templates/dummy.yaml new file mode 100644 index 000000000..a2b62c95a --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/chartfiletest.yaml b/internal/chart/v3/util/testdata/chartfiletest.yaml new file mode 100644 index 000000000..d222c8f8d --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/coleridge.yaml b/internal/chart/v3/util/testdata/coleridge.yaml new file mode 100644 index 000000000..b6579628b --- /dev/null +++ b/internal/chart/v3/util/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/internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.lock b/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.lock new file mode 100644 index 000000000..6fcc2ed9f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/Chart.yaml new file mode 100644 index 000000000..b8773d0d3 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/README.md b/internal/chart/v3/util/testdata/dependent-chart-alias/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..5bbae10af --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/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/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-alias/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3190136b050e62c628b3c817fd963ac9dc4a9e25 GIT binary patch literal 967 zcmV;&133I2iwFR+9h6)E1MQb_y%a`Bd>#= zhbqf2w!b6}wZ9@rvIhtwzm?~C&+QLm+G2!l%`$_aUgS(@pdjdX3a%E}VXVc7`)dg( zL%IRN2}c1D3xoMi2w@WuWOMZ?5i&3FelBVyq_Ce_8fREdP%NDf<&d!wu3{&VUQNs{N%vK$Ha~k^gB2!0bO7r0ic0 zbqCp*X#j?=|H@GNONsuE)&Idbh>2MX(Z}|+=@*sLod*wS?A8Ugp#mMPuMO0NnZmos9_rr z3xpDL+otj~lRh?B4hHFTl+d5-8NBXmUXB~EyF^bx7m*+!*g>obczvGF|8xkWsHN8; z%#+wiWP{=2pC*98@$VNzzsll&G#D7&11-;D>HT0x|DV2?6}a~*p46@R|2l??e_8dX z@Bb>D3t~VC_*wjq2Dy!6J-_5ME%%J+xmsbK5a#qO>ZV?Wt`d|TDdrB^B&LqFr@lYdUA zs&4-JAg3&uc4dA0gv*bXU9QePa9^8wR{HovA)j@)JOAIkak0Jl)aKqA_3XLM#?H=V z-{tlG=xy^os=1Z><53;&+C4=zp|%rwv!)UyY=%k_t%Y|wNRQl`C6N_Z&S;S+~wb1=)2Uh_4I81`y>DO zoLPSt=btB&x{CUK`0DA&){efW_O36RxnsYZNT0r;JbTLR{H4M3C$8(Cee==aQ~Gbq p$`5v3?NB{4-j0XQHf literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-alias/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-alias/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/util/testdata/dependent-chart-alias/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-alias/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore new file mode 100644 index 000000000..8a71bc82e --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/.helmignore @@ -0,0 +1,2 @@ +ignore/ +.* diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/Chart.yaml new file mode 100644 index 000000000..8b4ad8cdd --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/.ignore_me new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..5bbae10af --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-helmignore/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-helmignore/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/Chart.yaml new file mode 100644 index 000000000..8b4ad8cdd --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..5bbae10af --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3190136b050e62c628b3c817fd963ac9dc4a9e25 GIT binary patch literal 967 zcmV;&133I2iwFR+9h6)E1MQb_y%a`Bd>#= zhbqf2w!b6}wZ9@rvIhtwzm?~C&+QLm+G2!l%`$_aUgS(@pdjdX3a%E}VXVc7`)dg( zL%IRN2}c1D3xoMi2w@WuWOMZ?5i&3FelBVyq_Ce_8fREdP%NDf<&d!wu3{&VUQNs{N%vK$Ha~k^gB2!0bO7r0ic0 zbqCp*X#j?=|H@GNONsuE)&Idbh>2MX(Z}|+=@*sLod*wS?A8Ugp#mMPuMO0NnZmos9_rr z3xpDL+otj~lRh?B4hHFTl+d5-8NBXmUXB~EyF^bx7m*+!*g>obczvGF|8xkWsHN8; z%#+wiWP{=2pC*98@$VNzzsll&G#D7&11-;D>HT0x|DV2?6}a~*p46@R|2l??e_8dX z@Bb>D3t~VC_*wjq2Dy!6J-_5ME%%J+xmsbK5a#qO>ZV?Wt`d|TDdrB^B&LqFr@lYdUA zs&4-JAg3&uc4dA0gv*bXU9QePa9^8wR{HovA)j@)JOAIkak0Jl)aKqA_3XLM#?H=V z-{tlG=xy^os=1Z><53;&+C4=zp|%rwv!)UyY=%k_t%Y|wNRQl`C6N_Z&S;S+~wb1=)2Uh_4I81`y>DO zoLPSt=btB&x{CUK`0DA&){efW_O36RxnsYZNT0r;JbTLR{H4M3C$8(Cee==aQ~Gbq p$`5v3?NB{4-j0XQHf literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-no-requirements-yaml/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/Chart.yaml new file mode 100644 index 000000000..06283093e --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..5bbae10af --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3190136b050e62c628b3c817fd963ac9dc4a9e25 GIT binary patch literal 967 zcmV;&133I2iwFR+9h6)E1MQb_y%a`Bd>#= zhbqf2w!b6}wZ9@rvIhtwzm?~C&+QLm+G2!l%`$_aUgS(@pdjdX3a%E}VXVc7`)dg( zL%IRN2}c1D3xoMi2w@WuWOMZ?5i&3FelBVyq_Ce_8fREdP%NDf<&d!wu3{&VUQNs{N%vK$Ha~k^gB2!0bO7r0ic0 zbqCp*X#j?=|H@GNONsuE)&Idbh>2MX(Z}|+=@*sLod*wS?A8Ugp#mMPuMO0NnZmos9_rr z3xpDL+otj~lRh?B4hHFTl+d5-8NBXmUXB~EyF^bx7m*+!*g>obczvGF|8xkWsHN8; z%#+wiWP{=2pC*98@$VNzzsll&G#D7&11-;D>HT0x|DV2?6}a~*p46@R|2l??e_8dX z@Bb>D3t~VC_*wjq2Dy!6J-_5ME%%J+xmsbK5a#qO>ZV?Wt`d|TDdrB^B&LqFr@lYdUA zs&4-JAg3&uc4dA0gv*bXU9QePa9^8wR{HovA)j@)JOAIkak0Jl)aKqA_3XLM#?H=V z-{tlG=xy^os=1Z><53;&+C4=zp|%rwv!)UyY=%k_t%Y|wNRQl`C6N_Z&S;S+~wb1=)2Uh_4I81`y>DO zoLPSt=btB&x{CUK`0DA&){efW_O36RxnsYZNT0r;JbTLR{H4M3C$8(Cee==aQ~Gbq p$`5v3?NB{4-j0XQHf literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-all-in-requirements-yaml/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/Chart.yaml new file mode 100644 index 000000000..6543799d0 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..5bbae10af --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/charts/mariner-4.3.2.tgz new file mode 100644 index 0000000000000000000000000000000000000000..3190136b050e62c628b3c817fd963ac9dc4a9e25 GIT binary patch literal 967 zcmV;&133I2iwFR+9h6)E1MQb_y%a`Bd>#= zhbqf2w!b6}wZ9@rvIhtwzm?~C&+QLm+G2!l%`$_aUgS(@pdjdX3a%E}VXVc7`)dg( zL%IRN2}c1D3xoMi2w@WuWOMZ?5i&3FelBVyq_Ce_8fREdP%NDf<&d!wu3{&VUQNs{N%vK$Ha~k^gB2!0bO7r0ic0 zbqCp*X#j?=|H@GNONsuE)&Idbh>2MX(Z}|+=@*sLod*wS?A8Ugp#mMPuMO0NnZmos9_rr z3xpDL+otj~lRh?B4hHFTl+d5-8NBXmUXB~EyF^bx7m*+!*g>obczvGF|8xkWsHN8; z%#+wiWP{=2pC*98@$VNzzsll&G#D7&11-;D>HT0x|DV2?6}a~*p46@R|2l??e_8dX z@Bb>D3t~VC_*wjq2Dy!6J-_5ME%%J+xmsbK5a#qO>ZV?Wt`d|TDdrB^B&LqFr@lYdUA zs&4-JAg3&uc4dA0gv*bXU9QePa9^8wR{HovA)j@)JOAIkak0Jl)aKqA_3XLM#?H=V z-{tlG=xy^os=1Z><53;&+C4=zp|%rwv!)UyY=%k_t%Y|wNRQl`C6N_Z&S;S+~wb1=)2Uh_4I81`y>DO zoLPSt=btB&x{CUK`0DA&){efW_O36RxnsYZNT0r;JbTLR{H4M3C$8(Cee==aQ~Gbq p$`5v3?NB{4-j0XQHf literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml b/internal/chart/v3/util/testdata/dependent-chart-with-mixed-requirements-yaml/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz-1.2.3.tgz b/internal/chart/v3/util/testdata/frobnitz-1.2.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..8731dce02cc9603e7813a07670a7d057129a8b20 GIT binary patch literal 3485 zcmV;O4Px>iiwFRyACz1G1MOT3TolzBhwPeez8~c^`*9rfidUF@%`Sp~AdnJ>5AaDU z!|q{sVs>YnnFUtlqF_quB{faUuIRnpn-?EtYMQ2^*3EomX<1&iyrx3=%6GcL-ZQhy z0xPGp&V1kbUS~2{Cc{ke6XkwQ2Zcfrs?h-PsMU%`g^F+wfo zL8wDRm4reI6iT&PP51##6W)^>R*olGbSoqaAqQ_yhsZKB@6e9xIo!ub1erDSpOg?A zpPUlk6n&ua&=SNQ=3k`|=U<~xYK0d?p(NDmP(Pq(iktDo?|lAU(+(^&se?v_)XRbffp-h5waGrOJcjvSVHsT( zdQRkcK}mcT7$gN!tr8J-k|PV5Qh@+^r)C)|0KP1083K1oDmgsfQLI(HW7p#_@t z(5$0iy|E!_3mPx+32O&mfh%zZCSGKrh2bckVQmfHpiba(u1j7NX$%T z+c0_kHaLhH*NcrPHDW7-msVJ)7aEBW1|;esS}WcBpOBoA8k3ZS^SOLu_uft(?Lgz?Jv;jTES!i_RQ60%i@Y{f!|Iw^B zjrMo<`kzXx(h?e#p#Q0~YDxd|1>EzG3$`_7Ff;5O2I1b|RsJg#p7Nj2XeIgY3pi(7 zE=lv>Dct-&JU%9Fa6E3(H+~=9_+O<~dd7dWN`=J#zCa*&uEs|ztD_6L{Chz33gI$Y zU?3R5kp|ch5e^b~U?e$UW`I>7a?;1aY)CT}L6elpo?}>`cV2)j(lj%fV8B6$R7v!Y zv4qeH_Mb+rR!a7tFW{bkG3v-QNdka3`L85yk^dScA<2JVAP~d= z=hO?1YNYTnVnCh~PBJNjl@%k{NTYK~aZCuagJg7$$z&YViJ1XNe7j180wkFM30!5E zB%dE{G$8+L&T!t+IHJI-|A+AO|06QV|L+qFf;7#5ygXcF-ATwu%OtHd53n4DrS({T zzQn-4Y1H@sH;og>EB|Y5UH=Km|Kk(XCiT_H00x&Fg0Qn(5wBY@XY_xB6>;w`vRi;=ZuV% z)`JXWyNR=pPHm!Vo@Pk290Y?5t1|;cpfKxbs~(YvW}YiP@SvYU!o9+i|6Oj{5YaJ< z^M;9y(1#cPSB5Iw_Ft%0Bjo>v5`ev>0_=Rt#EnkN5v>S=o0%kDol#*S}rQF|YR=(R#*X?cx$zl|?P-#10D^-e$@| z{0|vZyInqGZ9F^zo{+Cx4n_p1kmM z5Fp7fX1^HwZQSEwFAPkcx+ihc?8LFF=0#7Iy*KBN12dCilNUBy(m~%jdUNXzbXc3J z)_$|--Cq{J+;0a+C_X!NdD$0#GJYF=T=)90`Im+bv0k13(e}yZGe2r~Z2Z-zEoDhZ z4{x70<>`Hn_k){{sg^envv-W!(R%-AeDaew)9i^$X9qo5w#X6x%Au`6C(eBDf29*E zJK&cz!7;zr>8(Acjjob6@0{NA`SN+m(FdD-v};T7@Co)|yw!A|taHRD|Gy{f^}k%0 zKF*Y)>-je_dce?bJ&3VOFZJ9sF7UI|MWa@~0xRc-6NfkV`+P))13$LOJK6tqR{6B9 z*?y~|7v-Jro4Po2|EI})UXF=+_QM-%B4f+*LdsuyZDhsOPJD-!%?eYBHU9F?4_q&8 zqnx+)a^LL{s}0yQNl!Zn|D?Yx;Z`dle3Q48M}Te81{2P*zTNi$KGCB=6<=Y`K4KkclaIaI#+zKU`6k5 z`V8H@x~M4a`~rOJFIP&A{sVB#)m8h85(nw&y&HO7sr-Ok*KjO&*3 zXO}nA7*uZ@3@Y7Xsa(H$)d9Q1w*30Rl}WotuibR~@JV*a+H+s*yxPB@x7|Mc+Lsps zORK7@_V!^G8xLLkabor4Po7OIt^Dlg>h6x6CDZfX+`7!J+`z86ezfr7?#+gP$hT6e z$V11Y@+kZJHceVqa!K<~H%$p;v2iJUEQthP_!*w(WO=Mn!|+vTdRN% zf&=`5?w9{>qyIPRZa}a4pH^L8|5FLqB>C?PG`RoI75uHY`!5b!XvT(Az6_G1bRb3~ z0ZEkEnhPw9zIG>|7$mNrQL$f0;6AZ0qn zDikr9z(TTU8RF4`Qq#CAIe}+Pbh<~JTDZTOpjAyEn_-2^vUKMyhM?X=RVE#{Lz^<$ z7{&^8b#}e*bqB)v=+4Kjvn__JNl2>Ulk`X4^>G{iZ`5p{yOsa$@BbkEPxAlz2Dh32 zM$HEDCjS+l`9CUDEPemSCkPZb`3dq^Q2lGNd`z|w=Zhfa@BGH$RsMUdq4&~Ow@s>R_T4U=b3W;sVgXu*Q9F!Tao;~ovkd1w3IL~^_;MD*y~M=Z!2|Eog@5B^tb zw9@+T69mj`(MddU^!DpR4jQh|4H~RXvW00f)FT!86b&^tB}_YH z{w;0n+-4Q8YN9Gj4;|bv``oORueJ1xUJO@1)@4*sRbExe>F1XR59#r*4SPnPFM6rv z;;5`6N2V*rt?N;-{Da;nmVEN&q#w)hj~xYJ@BDYyMWP#fApFD1Q75{}PQ>eiJFAjk z?Sel8M)dx8Q(8vbz7+)u>yW>cJ#lP&^^`?79liHnFL=3X%B5Xhbl+EYK@aNqq3Ej# zeytwxz&|AL&i~5$HUWRN|4)TPg0%nNC%BvWzu#bxH~Ej|-b4PY)GF!wPd-6|@gH&8 z>xX}0oF8Nq-wV;fmT1j@tQjH`q2bUECYg?p0`7+Y@7EdRjsI0jPy0`$(MtJ0K7p`L z{}$)DIPJJBu+Ar6$HWXy3PEKik{4nFf)8FGh=V#BjhtvRIo}gtAt{yvJR>9vT1bu) zQw7ma8)IeN4tP$eEK~xK02Av;;zEK12@)hokRU;V1PKx(NRS{wf&>W?BuJ1Tp;7RE LkUR>*0C)fZJ*e(i literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/frobnitz/.helmignore b/internal/chart/v3/util/testdata/frobnitz/.helmignore new file mode 100644 index 000000000..9973a57b8 --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/.helmignore @@ -0,0 +1 @@ +ignore/ diff --git a/internal/chart/v3/util/testdata/frobnitz/Chart.lock b/internal/chart/v3/util/testdata/frobnitz/Chart.lock new file mode 100644 index 000000000..6fcc2ed9f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/Chart.yaml new file mode 100644 index 000000000..1b63fc3e2 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/frobnitz/INSTALL.txt b/internal/chart/v3/util/testdata/frobnitz/INSTALL.txt new file mode 100644 index 000000000..2010438c2 --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/INSTALL.txt @@ -0,0 +1 @@ +This is an install document. The client may display this. diff --git a/internal/chart/v3/util/testdata/frobnitz/LICENSE b/internal/chart/v3/util/testdata/frobnitz/LICENSE new file mode 100644 index 000000000..6121943b1 --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/LICENSE @@ -0,0 +1 @@ +LICENSE placeholder. diff --git a/internal/chart/v3/util/testdata/frobnitz/README.md b/internal/chart/v3/util/testdata/frobnitz/README.md new file mode 100644 index 000000000..8cf4cc3d7 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/charts/_ignore_me b/internal/chart/v3/util/testdata/frobnitz/charts/_ignore_me new file mode 100644 index 000000000..2cecca682 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/Chart.yaml new file mode 100644 index 000000000..2a2c9c883 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/frobnitz/charts/alpine/README.md b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/README.md new file mode 100644 index 000000000..b30b949dd --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/Chart.yaml new file mode 100644 index 000000000..aea109c75 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast1/values.yaml new file mode 100644 index 000000000..42c39c262 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/charts/mast2-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..61cb62051110b55f3d08213dc81dcf0b1c2d8e53 GIT binary patch literal 252 zcmVDc zVQyr3R8em|NM&qo0PNJUs=_c72H?(liabH@pWIst-7YSIyL+rhEHrIN(t?QZE=F{y zgNRfS&$pa5Ly`mMk2OB%pV`*9knW7FlL-Joo@KED7*{C$d;N~z z37$S{+}wvSU9}|VtF|fRpv9Ve>8dWo|9?5B+RE}Y9CFh-x#(Bq8Vck^V=NUiPLCKa z8z5CF#JgK!4>;$4Fm+FUst4d+{(+nP|0&J+e}(;l^U4@w-{=?s0RR8vgVbLD3;+OM Cs&R<` literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/templates/alpine-pod.yaml new file mode 100644 index 000000000..5bbae10af --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml new file mode 100644 index 000000000..6c2aab7ba --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/charts/alpine/values.yaml @@ -0,0 +1,2 @@ +# The pod name +name: "my-alpine" diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/Chart.yaml new file mode 100644 index 000000000..4d3eea730 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/Chart.yaml new file mode 100644 index 000000000..da605991b --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml new file mode 100644 index 000000000..3121cd7ce --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/charts/albatross/values.yaml @@ -0,0 +1,4 @@ +albatross: "true" + +global: + author: Coleridge diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl new file mode 100644 index 000000000..29c11843a --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/templates/placeholder.tpl @@ -0,0 +1 @@ +# This is a placeholder. diff --git a/internal/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml b/internal/chart/v3/util/testdata/frobnitz/charts/mariner/values.yaml new file mode 100644 index 000000000..b0ccb0086 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz/docs/README.md b/internal/chart/v3/util/testdata/frobnitz/docs/README.md new file mode 100644 index 000000000..d40747caf --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/docs/README.md @@ -0,0 +1 @@ +This is a placeholder for documentation. diff --git a/internal/chart/v3/util/testdata/frobnitz/icon.svg b/internal/chart/v3/util/testdata/frobnitz/icon.svg new file mode 100644 index 000000000..892130606 --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/icon.svg @@ -0,0 +1,8 @@ + + + Example icon + + + diff --git a/internal/chart/v3/util/testdata/frobnitz/ignore/me.txt b/internal/chart/v3/util/testdata/frobnitz/ignore/me.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/frobnitz/templates/template.tpl b/internal/chart/v3/util/testdata/frobnitz/templates/template.tpl new file mode 100644 index 000000000..c651ee6a0 --- /dev/null +++ b/internal/chart/v3/util/testdata/frobnitz/templates/template.tpl @@ -0,0 +1 @@ +Hello {{.Name | default "world"}} diff --git a/internal/chart/v3/util/testdata/frobnitz/values.yaml b/internal/chart/v3/util/testdata/frobnitz/values.yaml new file mode 100644 index 000000000..61f501258 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz b/internal/chart/v3/util/testdata/frobnitz_backslash-1.2.3.tgz new file mode 100644 index 0000000000000000000000000000000000000000..6929659514d35d2a2fe1098c1c097194378cc3b5 GIT binary patch literal 3496 zcmV;Z4Oj9XiwFRyACz1G1MOT3ToiR4hwMx@-;eUlKKvc@#1m$BXJ3MVAdnJ>5AaC} z!|bp-F}t(P%mOQNsF;#^q^4=viQaXWhmSHfO;b_pG9Ou5mZz3yDwMB$rz`CJXLdnY zR~K|!wdVT~XLkO-`TzejzxnDQ%Ve1;enLhTPULVj&KczazJ@}f&}lS4+-kMr zR)MLVTNi**rB9msk`vbUz8cDJo#h4>Nu2N>k zO=JYP{h^TQNseGC3ojC;87U6He|Q}wD$A|y1yQ0HszPKrq%DtO^<0DufUr73@dsyN z86t<|#UFY`kLzV7oHFz95BB0b!8wyOBMAnPvf{bd6}_c?%aIm)9eZXETgE3uk3UM(A0f zXh?q*GsEM;trJHg^LZABcPGNRmy6R@VV*c7GLo#=;M#XK z=}8M|){|y}A{*QroVLLGC0^mJo3B8Xr~(lWvZRIKD4t<$o+n5Ma?aK3(1f!B1KfI4 zqbd|@QjZV%nU>Iac+me&F8a_7XsrFOR%x}EQX$xXT1+F^|Nejn{tGG%<#G)^8^as_ zHSnO@`ma!_r1jq)i0q$lqHDE%6e27h{i9`(ko^EQzy%BLMIo+*FE1|-%~PQaYmj5` zBIU3RagyDYL|i+Zz2(!CImZ*RGCVw7F18}|*B|GMKpv&%jZp}u(IRl=rwm5k^@Gw! zAw0?PNHjbvQZQ1Y6PybY5FX=5xp+uuV@ZM+$+IaI(t~{10|IQ~|E})Uid(&F;eIwn z(@}^S&$S`)hJA1(>|8H)qST1JsJ-;Ul6s+&Na2P=KSV2}2l-D-NllAQPDc5B-Zx`- z)&Dx|Hv1n_O7iay)S2>TK$+n+(lj*439E@T^QdTj6Ep?)p>1dbdWy5qaLdDUq#(g> z10Lib^52)%U*qjROp9sN8ceYNME_s1|9*XV;6Ez()|A1_uy?r~O@UYbD;4hgU#(M1 z`rjXL@>wov^WP~v$p7$!*!aN-s7dduF}%q?rc%1+f8a_W$-h4k44$iv%*yK7g083@ z5Mx28OcoprMsT=+^hAV$L>?G%4wxBW6^NYl@nkMZGZw*;l*yiBSfFW;3CH zBS~mV!av3mMhk8A;){}b=_lMV|3foGj*rIhCjTmhyZq}gtwNH2f8dczF6b!($?*|T zhveckrI+-?{lSC$^CZ0II8XY_32eImr-Q#F|IZ)rz<)98$oa4Uz#IRS>f7|cMukcG z-yaADvB1f2Ay5qsK1NK#bHYwGWw5eh-WpnTPA!fI$#swd_Ys+lBMGq{0IF|StIz;7 zGr)nX4UzQo!;QP@|Jps2x7?2^@T&jSZt;IisZ&Vtf4^W5Nz)7{C_qKgod}(@Y}`uo z0Lf!mT8|VKN&@_w#+~B-z6=6+m4D6c>%T^d|M&&BO8M$l0E28HT;oaux(t8^Jy8k2 z3hrG0<%|IM!Ut@u|EJdJFqIPef0#lktpAe#_v_=%{LgTvhB8d|odxvj|0&$+zc7VX zEBSwZfvEpEBV(oYAPf3#BG2KjO$^P`3=WrrP>^MHR)7c;W`efrA&Kbbx$*-K`YI&c zEAA}+u7GWr7@|da!-RVt!;AbwXRprx(`hvd!T(chbdvo013@!e1S|`h_d-CkvkS%< zI}Pks(e|;}r`{e=9vwenN5b1<-#@gux-T~(ZbH`quZ>PGjnCM5!u+x2BAUS@A}TQP*W9phfqD!0(ooe;4xj$uoTe+w~mtde0H9XFS#}KCxAK z^pZ~Ou<+q+CND()kTtd2SGB$k5Wax*VjB zSEoPu(;V}pg{MORj(;)x#kgm__gYvgGA{J3wN|*{RFRzxb2!+lb@D>%-<>8aBjwb^b@&CsoY+ zsNJ#gSEIL-Cm%h$ect4!_c`7VZ9b+(-aOpiF@8tu{iD%IPu@(oCoP>F@?`lUN5U(I zwuYQI^L^lzPOR)eKyrp-ey`J8drTW$EpOgAv*+^_^HO3CHv4GTmeAoD_F=r$bfCO* zEC+-crJSB6SDV6B?H#}y*&~81{W0zj)xoKSRXK9N@t$u~9njfJ)yt&`!BRU-T zu}#6r{-<*)rghB?SRJ#d;C$b-#o7BmP3iM;Z1l4q-dGbAS5XjF@zQG}E3bCqJG5*z zCACBoDF6Jx^|Cg~d228C-5$BxfIO4@v_l=3{8tBv3Y3M`%-nXOYB6nlD(T%PhFo@B zFCO#Hyj8~&?WY{?5*vS6@b$!-)6+gV>xi4N>!+e&KNp4X&Z}_j?X_j@m)n|Onx%M$ z-?6T99%ECk-r>S*YR4R#ZZ~~!+%bG+V93w<9}K1V8`9OG;L1d;BWR_;3^$M z4^AL<=2vE2x12w_yqU&;y>T$4Y>TC8{pwW*><-)V>jzgR?;gE&)A7S6*&%DseX;Xu z|DxV@`|xXDUI;F$uBqPJhgobqbnV9pHIqJhHmR)Yv!83aJ9d^%FL-n7GP`mEyXN}Q zDHnHdHUvezm0FD-Iv!m>+26Nm(sNQun}51#+Bff(M*dvAaeaD{eL8byWy-5t3v`^l z7n!-QqTt5s>;89e%;cq)=dL+?>Po-q(|7HYhrO{esqIfI$|@J%*mdpdZe@q!ZJCNL zt(wwI*|L+%g<-ZRC!QS|x!Zlf{kqUjPh8mk1t{pOR zq;~Kq`bGG#+tP+HQNkCw0?vH3x_rB$Q}bh=+h#<@o-aF+E<1{%Q#KW>7#ONHCGMY6 zavpt-Usha_)_&jmx3qC{n^nH5iLNR;ba2D$bF)^y)-oVwF}eD&E~7%K3#v;`KfgS5 zNRNMQ*faWk@k=ciN9PrlU60D;AM`%4?jI<=fArys=KiVB0j7d zb)u{6L;?}o8B2M!3;GNg(fi{~=~-?2Ru(C&L;h0!#If-;lNa4|^xk{D=;i9kmv(g_ zzOU*6FVyiv@mCoEtsd}=e@M_2`B&bf3)uAd9|qk($^Y{Qn#TWoO9px4Kjgie{#UEi z()XYI0+0D0k=YH?Ke5iwFp5uu7+_1XW|6F!gpPsd)DI%Lj*kMGCja*q4Du%bn9|+< zQ)_fm{f}QDyr+L#@LXgzDhsanq1Q37gOkHxn+fNI+<*{+7YO1>f`)fa^qHJb2e9B2 z%K@GdiUln=$BCta@TZNjvULG?PB>XO2}A)zJWo;=5+q2FAVGoz2@)hokRU;V1PKx( WNRS{wf&>XZ!T$k(as0gicmMzx5%RPE literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/genfrob.sh b/internal/chart/v3/util/testdata/genfrob.sh new file mode 100755 index 000000000..35fdd59f2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock new file mode 100644 index 000000000..b2f17fb39 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml new file mode 100644 index 000000000..0b3e9958b --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..d28e1621c86a56affb0617a912930d982ee5d09c GIT binary patch literal 333 zcmV-T0kZxdiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0PK}PYr`-Mg>%lY5bWGeZqs!5+TB+M-CZQ2GbE0Y9n>MvEZ~_~beXUgrQc z1sW=VuODc zVQyr3R8em|NM&qo0PK~)YJ)%!hCTZf13f1l6W36$d4NhGy$?F13%a|^u9EiYi-y+X zrIcVxVZX~T{~R1){(qg==KlCX61K0@waFSFA{Kc*RYY7?#Dhw*eUYg{p-|-sX1h%7 z6TnrrSY98ByIH2oEUQmBkeoRjtJ5jyR=-iu i)>JGtn?PqS;UNZ5Boc}Ioc90#0RR69wG({+3;+PL5}8~8 literal 0 HcmV?d00001 diff --git a/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml new file mode 100644 index 000000000..72427c097 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml new file mode 100644 index 000000000..38f03484d --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml new file mode 100644 index 000000000..058ab3942 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml new file mode 100644 index 000000000..10cc756b2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml new file mode 100644 index 000000000..976e5a8f1 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml b/internal/chart/v3/util/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml new file mode 100644 index 000000000..b812f0a33 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/joonix/Chart.yaml b/internal/chart/v3/util/testdata/joonix/Chart.yaml new file mode 100644 index 000000000..1860a3df1 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/joonix/charts/.gitkeep b/internal/chart/v3/util/testdata/joonix/charts/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/internal/chart/v3/util/testdata/subpop/Chart.yaml b/internal/chart/v3/util/testdata/subpop/Chart.yaml new file mode 100644 index 000000000..53e9ec502 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/README.md b/internal/chart/v3/util/testdata/subpop/README.md new file mode 100644 index 000000000..e43fbfe9c --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/Chart.yaml new file mode 100644 index 000000000..1539fb97d --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/Chart.yaml new file mode 100644 index 000000000..2755a821b --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/templates/service.yaml new file mode 100644 index 000000000..27501e1e0 --- /dev/null +++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartA/values.yaml new file mode 100644 index 000000000..f0381ae6a --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/Chart.yaml new file mode 100644 index 000000000..bf12fe8f3 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/templates/service.yaml new file mode 100644 index 000000000..27501e1e0 --- /dev/null +++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/charts/subchartB/values.yaml new file mode 100644 index 000000000..774fdd75c --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/crds/crdA.yaml new file mode 100644 index 000000000..fca77fd4b --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt new file mode 100644 index 000000000..4bdf443f6 --- /dev/null +++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/NOTES.txt @@ -0,0 +1 @@ +Sample notes for {{ .Chart.Name }} \ No newline at end of file diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml new file mode 100644 index 000000000..fee94dced --- /dev/null +++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/service.yaml @@ -0,0 +1,22 @@ +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 }} +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/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/role.yaml new file mode 100644 index 000000000..91b954e5f --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/rolebinding.yaml new file mode 100644 index 000000000..5d193f1a6 --- /dev/null +++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/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/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml new file mode 100644 index 000000000..7126c7d89 --- /dev/null +++ b/internal/chart/v3/util/testdata/subpop/charts/subchart1/templates/subdir/serviceaccount.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ .Chart.Name }}-sa diff --git a/internal/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart1/values.yaml new file mode 100644 index 000000000..a974e316a --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/Chart.yaml new file mode 100644 index 000000000..e77657040 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/Chart.yaml new file mode 100644 index 000000000..bf12fe8f3 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/templates/service.yaml new file mode 100644 index 000000000..fb3dfc445 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartB/values.yaml new file mode 100644 index 000000000..5e5b21065 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/Chart.yaml new file mode 100644 index 000000000..e8c0ef5e5 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/templates/service.yaml new file mode 100644 index 000000000..27501e1e0 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/charts/subchartC/values.yaml new file mode 100644 index 000000000..5e5b21065 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/templates/service.yaml new file mode 100644 index 000000000..27501e1e0 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml b/internal/chart/v3/util/testdata/subpop/charts/subchart2/values.yaml new file mode 100644 index 000000000..5e5b21065 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/noreqs/Chart.yaml b/internal/chart/v3/util/testdata/subpop/noreqs/Chart.yaml new file mode 100644 index 000000000..09eb05a96 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml b/internal/chart/v3/util/testdata/subpop/noreqs/templates/service.yaml new file mode 100644 index 000000000..27501e1e0 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/noreqs/values.yaml b/internal/chart/v3/util/testdata/subpop/noreqs/values.yaml new file mode 100644 index 000000000..4ed3b7ad3 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/subpop/values.yaml b/internal/chart/v3/util/testdata/subpop/values.yaml new file mode 100644 index 000000000..ba70ed406 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/test-values-invalid.schema.json b/internal/chart/v3/util/testdata/test-values-invalid.schema.json new file mode 100644 index 000000000..35a16a2c4 --- /dev/null +++ b/internal/chart/v3/util/testdata/test-values-invalid.schema.json @@ -0,0 +1 @@ + 1E1111111 diff --git a/internal/chart/v3/util/testdata/test-values-negative.yaml b/internal/chart/v3/util/testdata/test-values-negative.yaml new file mode 100644 index 000000000..5a1250bff --- /dev/null +++ b/internal/chart/v3/util/testdata/test-values-negative.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/internal/chart/v3/util/testdata/test-values.schema.json b/internal/chart/v3/util/testdata/test-values.schema.json new file mode 100644 index 000000000..4df89bbe8 --- /dev/null +++ b/internal/chart/v3/util/testdata/test-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/internal/chart/v3/util/testdata/test-values.yaml b/internal/chart/v3/util/testdata/test-values.yaml new file mode 100644 index 000000000..042dea664 --- /dev/null +++ b/internal/chart/v3/util/testdata/test-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/internal/chart/v3/util/testdata/three-level-dependent-chart/README.md b/internal/chart/v3/util/testdata/three-level-dependent-chart/README.md new file mode 100644 index 000000000..536bb9792 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/Chart.yaml new file mode 100644 index 000000000..1026f8901 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/Chart.yaml new file mode 100644 index 000000000..5bdf21570 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/Chart.yaml new file mode 100644 index 000000000..9bc306361 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml new file mode 100644 index 000000000..3728aa930 --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app1/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 1234 diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/Chart.yaml new file mode 100644 index 000000000..1313ce4e9 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/Chart.yaml new file mode 100644 index 000000000..9bc306361 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml new file mode 100644 index 000000000..98bd6d24b --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app2/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 8080 diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/Chart.yaml new file mode 100644 index 000000000..1a80533d0 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/Chart.yaml new file mode 100644 index 000000000..9bc306361 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml new file mode 100644 index 000000000..b738e2a57 --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app3/values.yaml @@ -0,0 +1,2 @@ +service: + type: ClusterIP diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/Chart.yaml new file mode 100644 index 000000000..886b4b1e4 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/Chart.yaml new file mode 100644 index 000000000..9bc306361 --- /dev/null +++ b/internal/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/templates/service.yaml new file mode 100644 index 000000000..3fd398b53 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/charts/library/values.yaml new file mode 100644 index 000000000..0c08b6cd2 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml new file mode 100644 index 000000000..8ed8ddf1f --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/templates/service.yaml @@ -0,0 +1 @@ +{{- include "library.service" . }} diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml new file mode 100644 index 000000000..3728aa930 --- /dev/null +++ b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/charts/app4/values.yaml @@ -0,0 +1,3 @@ +service: + type: ClusterIP + port: 1234 diff --git a/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml b/internal/chart/v3/util/testdata/three-level-dependent-chart/umbrella/values.yaml new file mode 100644 index 000000000..de0bafa51 --- /dev/null +++ b/internal/chart/v3/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/internal/chart/v3/util/validate_name.go b/internal/chart/v3/util/validate_name.go new file mode 100644 index 000000000..6595e085d --- /dev/null +++ b/internal/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/internal/chart/v3/util/validate_name_test.go b/internal/chart/v3/util/validate_name_test.go new file mode 100644 index 000000000..cfc62a0f7 --- /dev/null +++ b/internal/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/internal/chart/v3/util/values.go b/internal/chart/v3/util/values.go new file mode 100644 index 000000000..8e1a14b45 --- /dev/null +++ b/internal/chart/v3/util/values.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 util + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "sigs.k8s.io/yaml" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +// 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) (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.Chart, chrtVals map[string]interface{}, options ReleaseOptions, caps *Capabilities, skipSchemaValidation bool) (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", + }, + } + + vals, err := CoalesceValues(chrt, chrtVals) + if err != nil { + return 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 +} + +// 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/internal/chart/v3/util/values_test.go b/internal/chart/v3/util/values_test.go new file mode 100644 index 000000000..34c664581 --- /dev/null +++ b/internal/chart/v3/util/values_test.go @@ -0,0 +1,293 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "fmt" + "testing" + "text/template" + + chart "helm.sh/helm/v4/internal/chart/v3" +) + +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 := ToRenderValuesWithSchemaValidation(c, overrideValues, o, nil, false) + 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{}) { + 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") + } + } +} From 7007d4d485a89e8c9364311b7aee6276ab038d0a Mon Sep 17 00:00:00 2001 From: Mikel Olasagasti Uranga Date: Fri, 25 Jul 2025 22:14:17 +0200 Subject: [PATCH 16/21] chore(deps): remove phayes/freeport module Replaces the `phayes/freeport` module with the standard library's `net.Listen("tcp", "127.0.0.1:0")` idiom. This removes an unnecessary dependency and simplifies the codebase. Signed-off-by: Mikel Olasagasti Uranga --- go.mod | 1 - go.sum | 2 -- pkg/registry/utils_test.go | 7 ++++--- pkg/repo/repotest/server.go | 8 +++++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index e7978c530..9b23f4b9d 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,6 @@ require ( github.com/mitchellh/copystructure v1.2.0 github.com/moby/term v0.5.2 github.com/opencontainers/image-spec v1.1.1 - github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 github.com/rubenv/sql-migrate v1.8.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.9.1 diff --git a/go.sum b/go.sum index 464ad8590..f7789cbbb 100644 --- a/go.sum +++ b/go.sum @@ -249,8 +249,6 @@ github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJw 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= diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go index b270e51cc..f4ff5bd58 100644 --- a/pkg/registry/utils_test.go +++ b/pkg/registry/utils_test.go @@ -37,7 +37,6 @@ import ( _ "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" @@ -127,12 +126,14 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { // Registry config config := &configuration.Configuration{} - port, err := freeport.GetFreePort() + ln, err := net.Listen("tcp", "127.0.0.1:0") suite.Nil(err, "no error finding free port for test registry") + defer 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) suite.srv, err = mockdns.NewServer(map[string]mockdns.Zone{ "helm-test-registry.": { @@ -142,7 +143,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { suite.Nil(err, "no error creating mock DNS server") suite.srv.PatchNet(net.DefaultResolver) - config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%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{}{}} diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index ea9d5290c..7ff028b90 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -18,6 +18,7 @@ package repotest import ( "crypto/tls" "fmt" + "net" "net/http" "net/http/httptest" "os" @@ -29,7 +30,6 @@ import ( "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" @@ -176,12 +176,14 @@ func NewOCIServer(t *testing.T, dir string) (*OCIServer, error) { // Registry config config := &configuration.Configuration{} - port, err := freeport.GetFreePort() + 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() - config.HTTP.Addr = fmt.Sprintf("127.0.0.1:%d", port) + 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{ From 85243914a4dcb179b997e277f42d412329fbdf9a Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 30 Jul 2025 12:14:09 +0100 Subject: [PATCH 17/21] feat: switch yaml library to go.yaml.in/yaml/v3 Signed-off-by: Evans Mungai --- go.mod | 4 ++-- pkg/action/hooks.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index e7978c530..a8991569e 100644 --- a/go.mod +++ b/go.mod @@ -31,10 +31,10 @@ require ( github.com/spf13/cobra v1.9.1 github.com/spf13/pflag v1.0.7 github.com/stretchr/testify v1.10.0 + go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.40.0 golang.org/x/term v0.33.0 golang.org/x/text v0.27.0 - gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.33.3 k8s.io/apiextensions-apiserver v0.33.3 k8s.io/apimachinery v0.33.3 @@ -156,7 +156,6 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.4.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.25.0 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect @@ -171,6 +170,7 @@ require ( gopkg.in/evanphx/json-patch.v4 v4.12.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 k8s.io/component-base v0.33.3 // indirect k8s.io/kube-openapi v0.0.0-20250701173324-9bd5c66d9911 // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 1213e87e2..d01ec84a0 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -25,7 +25,7 @@ import ( "helm.sh/helm/v4/pkg/kube" - "gopkg.in/yaml.v3" + "go.yaml.in/yaml/v3" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" release "helm.sh/helm/v4/pkg/release/v1" From 008bd7fc8298c9f2e29e64c0ba4f63abf7bedb12 Mon Sep 17 00:00:00 2001 From: Atish Kumar Date: Fri, 1 Aug 2025 12:31:07 +0530 Subject: [PATCH 18/21] test(pkg/kube/client): add test for isReachable Signed-off-by: Atish Kumar --- pkg/kube/client_test.go | 105 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index cd83a7f9e..5ffa0972b 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -18,6 +18,7 @@ package kube import ( "bytes" + "errors" "io" "net/http" "strings" @@ -34,7 +35,9 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" jsonserializer "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes" k8sfake "k8s.io/client-go/kubernetes/fake" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest/fake" @@ -1079,3 +1082,105 @@ func TestCreatePatchCustomResourceSpec(t *testing.T) { 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.NewSimpleClientset() + 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) + } + } + }) + } +} From 4e483d36bd3ab9e197acc98c702f3e5c8b43ca8a Mon Sep 17 00:00:00 2001 From: Carlos Sanchez Date: Thu, 20 Mar 2025 19:04:49 +0000 Subject: [PATCH 19/21] fix: prevent panic when ChartDownloader.getOciURI needs to lookup tags because no version is provided but no RegistryClient is provided Signed-off-by: Carlos Sanchez --- pkg/downloader/chart_downloader.go | 4 ++++ pkg/downloader/chart_downloader_test.go | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 04c56e614..529fd788e 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -164,6 +164,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er } if registry.IsOCI(u.String()) { + if c.RegistryClient == nil { + return nil, fmt.Errorf("unable to lookup ref %s at version '%s', missing registry client", ref, version) + } + return c.RegistryClient.ValidateReference(ref, version, u) } diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 766afede1..a2e09eae5 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -23,6 +23,7 @@ import ( "helm.sh/helm/v4/internal/test/ensure" "helm.sh/helm/v4/pkg/cli" "helm.sh/helm/v4/pkg/getter" + "helm.sh/helm/v4/pkg/registry" "helm.sh/helm/v4/pkg/repo" "helm.sh/helm/v4/pkg/repo/repotest" ) @@ -60,10 +61,17 @@ func TestResolveChartRef(t *testing.T) { {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, From d4ed9210df1e5e940f6b6495b631641c464a23d4 Mon Sep 17 00:00:00 2001 From: Pavani Pogula Date: Wed, 6 Aug 2025 21:56:06 +0530 Subject: [PATCH 20/21] test(pkg/kube/roundtripper): Add unit tests for roundtripper.go Signed-off-by: Pavani Pogula --- pkg/kube/roundtripper_test.go | 161 ++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 pkg/kube/roundtripper_test.go diff --git a/pkg/kube/roundtripper_test.go b/pkg/kube/roundtripper_test.go new file mode 100644 index 000000000..96602c1f4 --- /dev/null +++ b/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 +} From 6597fecce392481407ad60d6ee5ce2000d7b5cab Mon Sep 17 00:00:00 2001 From: Pavani Pogula Date: Wed, 6 Aug 2025 21:56:33 +0530 Subject: [PATCH 21/21] test(pkg/kube/wait): Add unit tests for waitForPodSuccess, waitForJob and SelectorsForObject. Signed-off-by: Pavani Pogula --- pkg/kube/wait_test.go | 467 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 pkg/kube/wait_test.go diff --git a/pkg/kube/wait_test.go b/pkg/kube/wait_test.go new file mode 100644 index 000000000..d96f2c486 --- /dev/null +++ b/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) + } + }) + } +}