From 1e22b2fe7c06c771d5dd7177f4a13a510621b295 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Sat, 16 Aug 2025 16:23:29 -0600 Subject: [PATCH 001/118] fix: remove redundant error check Signed-off-by: Terry Howe --- pkg/kube/client.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 016055392..a78ffd596 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -269,10 +269,6 @@ func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) ( return nil, fmt.Errorf("invalid client create option(s): %w", err) } - if createOptions.forceConflicts && !createOptions.serverSideApply { - return nil, fmt.Errorf("invalid operation: force conflicts can only be used with server-side apply") - } - makeCreateApplyFunc := func() func(target *resource.Info) error { if createOptions.serverSideApply { slog.Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective))) From 284bd980b67f2be59b989abcc30ed2549936f2a6 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 27 Aug 2025 04:52:18 -0600 Subject: [PATCH 002/118] fix: replace pkg/engine regular expressions with parser Signed-off-by: Terry Howe --- pkg/engine/engine.go | 131 ++++++++++++++++++++++++++++--------------- 1 file changed, 85 insertions(+), 46 deletions(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 6e47a0e39..b42fa3219 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -34,18 +34,6 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" ) -// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 -// > "template: %s: executing %q at <%s>: %s" -var execErrFmt = regexp.MustCompile(`^template: (?P(?U).+): executing (?P(?U).+) at (?P(?U).+): (?P(?U).+)(?P( template:.*)?)$`) - -// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 -// > "template: %s: %s" -var execErrFmtWithoutTemplate = regexp.MustCompile(`^template: (?P(?U).+): (?P.*)(?P( template:.*)?)$`) - -// taken from https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 -// > "template: no template %q associated with template %q" -var execErrNoTemplateAssociated = regexp.MustCompile(`^template: no template (?P.*) associated with template (?P(.*)?)$`) - // Engine is an implementation of the Helm rendering implementation for templates. type Engine struct { // If strict is enabled, template rendering will fail if a template references @@ -338,7 +326,7 @@ func cleanupParseError(filename string, err error) error { location := tokens[1] // The remaining tokens make up a stacktrace-like chain, ending with the relevant error errMsg := tokens[len(tokens)-1] - return fmt.Errorf("parse error at (%s): %s", string(location), errMsg) + return fmt.Errorf("parse error at (%s): %s", location, errMsg) } type TraceableError struct { @@ -350,25 +338,97 @@ type TraceableError struct { func (t TraceableError) String() string { var errorString strings.Builder if t.location != "" { - fmt.Fprintf(&errorString, "%s\n ", t.location) + _, _ = fmt.Fprintf(&errorString, "%s\n ", t.location) } if t.executedFunction != "" { - fmt.Fprintf(&errorString, "%s\n ", t.executedFunction) + _, _ = fmt.Fprintf(&errorString, "%s\n ", t.executedFunction) } if t.message != "" { - fmt.Fprintf(&errorString, "%s\n", t.message) + _, _ = fmt.Fprintf(&errorString, "%s\n", t.message) } return errorString.String() } +// parseTemplateExecErrorString parses a template execution error string from text/template +// without using regular expressions. It returns a TraceableError and true if parsing succeeded. +func parseTemplateExecErrorString(s string) (TraceableError, bool) { + const prefix = "template: " + if !strings.HasPrefix(s, prefix) { + return TraceableError{}, false + } + remainder := s[len(prefix):] + + // Special case: "template: no template %q associated with template %q" + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 + if strings.HasPrefix(remainder, "no template ") { + return TraceableError{message: s}, true + } + + // Executing form: ": executing \"\" at <>: [ template:...]" + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 + if idx := strings.Index(remainder, ": executing "); idx != -1 { + templateName := remainder[:idx] + after := remainder[idx+len(": executing "):] + if len(after) == 0 || after[0] != '"' { + return TraceableError{}, false + } + // find closing quote for function name + endQuote := strings.IndexByte(after[1:], '"') + if endQuote == -1 { + return TraceableError{}, false + } + endQuote++ // account for offset we started at 1 + functionName := after[1:endQuote] + afterFunc := after[endQuote+1:] + + // expect: " at <" then location then ">: " then message + const atPrefix = " at <" + if !strings.HasPrefix(afterFunc, atPrefix) { + return TraceableError{}, false + } + afterAt := afterFunc[len(atPrefix):] + endLoc := strings.Index(afterAt, ">: ") + if endLoc == -1 { + return TraceableError{}, false + } + locationName := afterAt[:endLoc] + errMsg := afterAt[endLoc+len(">: "):] + + // trim chained next error starting with space + "template:" if present + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{ + location: templateName, + message: errMsg, + executedFunction: "executing \"" + functionName + "\" at <" + locationName + ">:", + }, true + } + + // Simple form: ": " + // Use LastIndex to avoid splitting colons within line:col info. + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 + if sep := strings.LastIndex(remainder, ": "); sep != -1 { + templateName := remainder[:sep] + errMsg := remainder[sep+2:] + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{location: templateName, message: errMsg}, true + } + + return TraceableError{}, false +} + // reformatExecErrorMsg takes an error message for template rendering and formats it into a formatted // multi-line error string func reformatExecErrorMsg(filename string, err error) error { - // This function matches the error message against regex's for the text/template package. - // If the regex's can parse out details from that error message such as the line number, template it failed on, + // This function parses the error message produced by text/template package. + // If it can parse out details from that error message such as the line number, template it failed on, // and error description, then it will construct a new error that displays these details in a structured way. // If there are issues with parsing the error message, the err passed into the function should return instead. - if _, isExecError := err.(template.ExecError); !isExecError { + var execError template.ExecError + if !errors.As(err, &execError) { return err } @@ -384,45 +444,24 @@ func reformatExecErrorMsg(filename string, err error) error { parts := warnRegex.FindStringSubmatch(tokens[2]) if len(parts) >= 2 { - return fmt.Errorf("execution error at (%s): %s", string(location), parts[1]) + return fmt.Errorf("execution error at (%s): %s", location, parts[1]) } current := err - fileLocations := []TraceableError{} + var fileLocations []TraceableError for current != nil { - var traceable TraceableError - if matches := execErrFmt.FindStringSubmatch(current.Error()); matches != nil { - templateName := matches[execErrFmt.SubexpIndex("templateName")] - functionName := matches[execErrFmt.SubexpIndex("functionName")] - locationName := matches[execErrFmt.SubexpIndex("location")] - errMsg := matches[execErrFmt.SubexpIndex("errMsg")] - traceable = TraceableError{ - location: templateName, - message: errMsg, - executedFunction: "executing " + functionName + " at " + locationName + ":", - } - } else if matches := execErrFmtWithoutTemplate.FindStringSubmatch(current.Error()); matches != nil { - templateName := matches[execErrFmt.SubexpIndex("templateName")] - errMsg := matches[execErrFmt.SubexpIndex("errMsg")] - traceable = TraceableError{ - location: templateName, - message: errMsg, - } - } else if matches := execErrNoTemplateAssociated.FindStringSubmatch(current.Error()); matches != nil { - traceable = TraceableError{ - message: current.Error(), + if tr, ok := parseTemplateExecErrorString(current.Error()); ok { + if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != tr { + fileLocations = append(fileLocations, tr) } } else { return err } - if len(fileLocations) == 0 || fileLocations[len(fileLocations)-1] != traceable { - fileLocations = append(fileLocations, traceable) - } current = errors.Unwrap(current) } var finalErrorString strings.Builder for _, fileLocation := range fileLocations { - fmt.Fprintf(&finalErrorString, "%s", fileLocation.String()) + _, _ = fmt.Fprintf(&finalErrorString, "%s", fileLocation.String()) } return errors.New(strings.TrimSpace(finalErrorString.String())) From 6273f9b38e22632b3b36b3cd142ad461f97c5e96 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 27 Aug 2025 08:18:26 -0600 Subject: [PATCH 003/118] fix: flaky registry data race on mockdns close Signed-off-by: Terry Howe --- pkg/registry/client_http_test.go | 5 +---- pkg/registry/client_insecure_tls_test.go | 5 +---- pkg/registry/client_tls_test.go | 5 +---- pkg/registry/utils_test.go | 20 ++++++++++---------- 4 files changed, 13 insertions(+), 22 deletions(-) diff --git a/pkg/registry/client_http_test.go b/pkg/registry/client_http_test.go index 043fd4205..dddd29ee9 100644 --- a/pkg/registry/client_http_test.go +++ b/pkg/registry/client_http_test.go @@ -32,10 +32,7 @@ type HTTPRegistryClientTestSuite struct { func (suite *HTTPRegistryClientTestSuite) SetupSuite() { // init test client - dockerRegistry := setup(&suite.TestSuite, false, false) - - // Start Docker registry - go dockerRegistry.ListenAndServe() + setup(&suite.TestSuite, false, false) } func (suite *HTTPRegistryClientTestSuite) TearDownSuite() { diff --git a/pkg/registry/client_insecure_tls_test.go b/pkg/registry/client_insecure_tls_test.go index accbf1670..03354475a 100644 --- a/pkg/registry/client_insecure_tls_test.go +++ b/pkg/registry/client_insecure_tls_test.go @@ -29,10 +29,7 @@ type InsecureTLSRegistryClientTestSuite struct { func (suite *InsecureTLSRegistryClientTestSuite) SetupSuite() { // init test client - dockerRegistry := setup(&suite.TestSuite, true, true) - - // Start Docker registry - go dockerRegistry.ListenAndServe() + setup(&suite.TestSuite, true, true) } func (suite *InsecureTLSRegistryClientTestSuite) TearDownSuite() { diff --git a/pkg/registry/client_tls_test.go b/pkg/registry/client_tls_test.go index 0897858b5..2bf1750a9 100644 --- a/pkg/registry/client_tls_test.go +++ b/pkg/registry/client_tls_test.go @@ -31,10 +31,7 @@ type TLSRegistryClientTestSuite struct { func (suite *TLSRegistryClientTestSuite) SetupSuite() { // init test client - dockerRegistry := setup(&suite.TestSuite, true, false) - - // Start Docker registry - go dockerRegistry.ListenAndServe() + setup(&suite.TestSuite, true, false) } func (suite *TLSRegistryClientTestSuite) TearDownSuite() { diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go index b46317fc6..781f3dd75 100644 --- a/pkg/registry/utils_test.go +++ b/pkg/registry/utils_test.go @@ -29,7 +29,6 @@ import ( "os" "path/filepath" "strings" - "sync" "time" "github.com/distribution/distribution/v3/configuration" @@ -65,12 +64,13 @@ type TestSuite struct { CompromisedRegistryHost string WorkspaceDir string RegistryClient *Client + dockerRegistry *registry.Registry // A mock DNS server needed for TLS connection testing. srv *mockdns.Server } -func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { +func setup(suite *TestSuite, tlsEnabled, insecure bool) { suite.WorkspaceDir = testWorkspaceDir os.RemoveAll(suite.WorkspaceDir) os.Mkdir(suite.WorkspaceDir, 0700) @@ -166,20 +166,20 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) *registry.Registry { config.HTTP.TLS.ClientCAs = []string{tlsCA} } } - dockerRegistry, err := registry.NewRegistry(context.Background(), config) + suite.dockerRegistry, err = registry.NewRegistry(context.Background(), config) suite.Nil(err, "no error creating test registry") suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() - return dockerRegistry + go func() { + _ = suite.dockerRegistry.ListenAndServe() + _ = suite.srv.Close() + mockdns.UnpatchNet(net.DefaultResolver) + }() } func teardown(suite *TestSuite) { - var lock sync.Mutex - lock.Lock() - defer lock.Unlock() - if suite.srv != nil { - mockdns.UnpatchNet(net.DefaultResolver) - suite.srv.Close() + if suite.dockerRegistry != nil { + _ = suite.dockerRegistry.Shutdown(context.Background()) } } From ce97a2449e24bc7985e1ad614b0a8b1713d10894 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 27 Aug 2025 10:41:46 -0600 Subject: [PATCH 004/118] fix: move mockdns to packge level Signed-off-by: Terry Howe --- pkg/registry/main_test.go | 51 ++++++++++++++++++++++++++++++++++++++ pkg/registry/utils_test.go | 13 ---------- 2 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 pkg/registry/main_test.go diff --git a/pkg/registry/main_test.go b/pkg/registry/main_test.go new file mode 100644 index 000000000..4f6e11e4f --- /dev/null +++ b/pkg/registry/main_test.go @@ -0,0 +1,51 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "net" + "os" + "testing" + + "github.com/foxcpp/go-mockdns" +) + +func TestMain(m *testing.M) { + // A mock DNS server needed for TLS connection testing. + var srv *mockdns.Server + var err error + + srv, err = mockdns.NewServer(map[string]mockdns.Zone{ + "helm-test-registry.": { + A: []string{"127.0.0.1"}, + }, + }, false) + if err != nil { + panic(err) + } + + saveDialFunction := net.DefaultResolver.Dial + srv.PatchNet(net.DefaultResolver) + + // Run all tests in the package + code := m.Run() + + net.DefaultResolver.Dial = saveDialFunction + _ = srv.Close() + + os.Exit(code) +} diff --git a/pkg/registry/utils_test.go b/pkg/registry/utils_test.go index 781f3dd75..1da90566f 100644 --- a/pkg/registry/utils_test.go +++ b/pkg/registry/utils_test.go @@ -35,7 +35,6 @@ import ( "github.com/distribution/distribution/v3/registry" _ "github.com/distribution/distribution/v3/registry/auth/htpasswd" _ "github.com/distribution/distribution/v3/registry/storage/driver/inmemory" - "github.com/foxcpp/go-mockdns" "github.com/stretchr/testify/suite" "golang.org/x/crypto/bcrypt" @@ -65,9 +64,6 @@ type TestSuite struct { WorkspaceDir string RegistryClient *Client dockerRegistry *registry.Registry - - // A mock DNS server needed for TLS connection testing. - srv *mockdns.Server } func setup(suite *TestSuite, tlsEnabled, insecure bool) { @@ -135,13 +131,6 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) { // 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.": { - A: []string{"127.0.0.1"}, - }, - }, false) - suite.Nil(err, "no error creating mock DNS server") - suite.srv.PatchNet(net.DefaultResolver) config.HTTP.Addr = ln.Addr().String() config.HTTP.DrainTimeout = time.Duration(10) * time.Second @@ -172,8 +161,6 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) { suite.CompromisedRegistryHost = initCompromisedRegistryTestServer() go func() { _ = suite.dockerRegistry.ListenAndServe() - _ = suite.srv.Close() - mockdns.UnpatchNet(net.DefaultResolver) }() } From b12cd28503ffdc8fc28d14cd635690e38def629f Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 27 Aug 2025 04:02:28 -0600 Subject: [PATCH 005/118] fix: installer action goroutine count Signed-off-by: Terry Howe --- pkg/action/install.go | 11 ++++++++++- pkg/action/install_test.go | 29 +++++++++++++---------------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 276009b5c..b5b45bd42 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -30,6 +30,7 @@ import ( "path/filepath" "strings" "sync" + "sync/atomic" "text/template" "time" @@ -126,7 +127,8 @@ type Install struct { TakeOwnership bool PostRenderer postrender.PostRenderer // Lock to control raceconditions when the process receives a SIGTERM - Lock sync.Mutex + Lock sync.Mutex + goroutineCount atomic.Int32 } // ChartPathOptions captures common options used for controlling chart paths @@ -446,8 +448,10 @@ func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, t resultChan := make(chan Msg, 1) go func() { + i.goroutineCount.Add(1) rel, err := i.performInstall(rel, toBeAdopted, resources) resultChan <- Msg{rel, err} + i.goroutineCount.Add(-1) }() select { case <-ctx.Done(): @@ -458,6 +462,11 @@ func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, t } } +// getGoroutineCount return the number of running routines +func (i *Install) getGoroutineCount() int32 { + return i.goroutineCount.Load() +} + // isDryRun returns true if Upgrade is set to run as a DryRun func (i *Install) isDryRun() bool { if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index f567b3df4..fa9cfb222 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -28,7 +28,6 @@ import ( "os" "path/filepath" "regexp" - "runtime" "strings" "testing" "time" @@ -330,8 +329,8 @@ func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) { } rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.Equal("with-notes", rel.Name) is.NoError(err) + is.Equal("with-notes", rel.Name) is.Equal("parent", rel.Info.Notes) is.Equal(rel.Info.Description, "Install complete") } @@ -349,8 +348,8 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { } rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) - is.Equal("with-notes", rel.Name) is.NoError(err) + is.Equal("with-notes", rel.Name) // test run can return as either 'parent\nchild' or 'child\nparent' if !strings.Contains(rel.Info.Notes, "parent") && !strings.Contains(rel.Info.Notes, "child") { t.Fatalf("Expected 'parent\nchild' or 'child\nparent', got '%s'", rel.Info.Notes) @@ -454,9 +453,7 @@ func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { if err == nil { t.Fatalf("Install should fail containing error: %s", expectedErr) } - if err != nil { - is.Contains(err.Error(), expectedErr) - } + is.Contains(err.Error(), expectedErr) } func TestInstallRelease_NoHooks(t *testing.T) { @@ -541,14 +538,14 @@ func TestInstallRelease_Wait(t *testing.T) { instAction.WaitStrategy = kube.StatusWatcherStrategy vals := map[string]interface{}{} - goroutines := runtime.NumGoroutine() + goroutines := instAction.getGoroutineCount() res, err := instAction.Run(buildChart(), vals) is.Error(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, release.StatusFailed) - is.Equal(goroutines, runtime.NumGoroutine()) + is.Equal(goroutines, instAction.getGoroutineCount()) } func TestInstallRelease_Wait_Interrupted(t *testing.T) { is := assert.New(t) @@ -563,15 +560,15 @@ func TestInstallRelease_Wait_Interrupted(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(time.Second, cancel) - goroutines := runtime.NumGoroutine() + goroutines := instAction.getGoroutineCount() _, err := instAction.RunWithContext(ctx, buildChart(), vals) is.Error(err) is.Contains(err.Error(), "context canceled") - is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background - time.Sleep(10 * time.Second) // wait for goroutine to finish - is.Equal(goroutines, runtime.NumGoroutine()) + is.Equal(goroutines+1, instAction.getGoroutineCount()) // installation goroutine still is in background + time.Sleep(10 * time.Second) // wait for goroutine to finish + is.Equal(goroutines, instAction.getGoroutineCount()) } func TestInstallRelease_WaitForJobs(t *testing.T) { is := assert.New(t) @@ -647,7 +644,7 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(time.Second, cancel) - goroutines := runtime.NumGoroutine() + goroutines := instAction.getGoroutineCount() res, err := instAction.RunWithContext(ctx, buildChart(), vals) is.Error(err) @@ -659,9 +656,9 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { _, err = instAction.cfg.Releases.Get(res.Name, res.Version) is.Error(err) is.Equal(err, driver.ErrReleaseNotFound) - is.Equal(goroutines+1, runtime.NumGoroutine()) // installation goroutine still is in background - time.Sleep(10 * time.Second) // wait for goroutine to finish - is.Equal(goroutines, runtime.NumGoroutine()) + is.Equal(goroutines+1, instAction.getGoroutineCount()) // installation goroutine still is in background + time.Sleep(10 * time.Second) // wait for goroutine to finish + is.Equal(goroutines, instAction.getGoroutineCount()) } func TestNameTemplate(t *testing.T) { From 38d1a7376ff77a609874b3263427f711da946e32 Mon Sep 17 00:00:00 2001 From: Kamil Swiechowski Date: Fri, 11 Jul 2025 16:52:58 +0200 Subject: [PATCH 006/118] fix: throw warning when chart version is not semverv2 Signed-off-by: Kamil Swiechowski --- pkg/chart/v2/lint/lint_test.go | 13 +++++-- pkg/chart/v2/lint/rules/chartfile.go | 11 ++++++ pkg/chart/v2/lint/rules/chartfile_test.go | 39 ++++++++++++++++++- pkg/chart/v2/metadata.go | 2 +- ...hart-with-bad-subcharts-with-subcharts.txt | 1 + 5 files changed, 59 insertions(+), 7 deletions(-) diff --git a/pkg/chart/v2/lint/lint_test.go b/pkg/chart/v2/lint/lint_test.go index 3c777e2bb..bd3ec1f1f 100644 --- a/pkg/chart/v2/lint/lint_test.go +++ b/pkg/chart/v2/lint/lint_test.go @@ -42,12 +42,12 @@ const invalidChartFileDir = "rules/testdata/invalidchartfile" func TestBadChart(t *testing.T) { m := RunAll(badChartDir, values, namespace).Messages - if len(m) != 8 { + if len(m) != 9 { t.Errorf("Number of errors %v", len(m)) t.Errorf("All didn't fail with expected errors, got %#v", m) } - // There should be one INFO, one WARNING, and 2 ERROR messages, check for them - var i, w, e, e2, e3, e4, e5, e6 bool + // There should be one INFO, 2 WARNING and 2 ERROR messages, check for them + var i, w, w2, e, e2, e3, e4, e5, e6 bool for _, msg := range m { if msg.Severity == support.InfoSev { if strings.Contains(msg.Err.Error(), "icon is recommended") { @@ -83,8 +83,13 @@ func TestBadChart(t *testing.T) { e6 = true } } + if msg.Severity == support.WarningSev { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") { + w2 = true + } + } } - if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w { + if !e || !e2 || !e3 || !e4 || !e5 || !i || !e6 || !w || !w2 { t.Errorf("Didn't find all the expected errors, got %#v", m) } } diff --git a/pkg/chart/v2/lint/rules/chartfile.go b/pkg/chart/v2/lint/rules/chartfile.go index 185f524a4..806363477 100644 --- a/pkg/chart/v2/lint/rules/chartfile.go +++ b/pkg/chart/v2/lint/rules/chartfile.go @@ -67,6 +67,7 @@ func Chartfile(linter *support.Linter) { linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartIconURL(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartType(chartFile)) linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartDependencies(chartFile)) + linter.RunLinterRule(support.WarningSev, chartFileName, validateChartVersionStrictSemVerV2(chartFile)) } func validateChartVersionType(data map[string]interface{}) error { @@ -158,6 +159,16 @@ func validateChartVersion(cf *chart.Metadata) error { return nil } +func validateChartVersionStrictSemVerV2(cf *chart.Metadata) error { + _, err := semver.StrictNewVersion(cf.Version) + + if err != nil { + return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version) + } + + return nil +} + func validateChartMaintainer(cf *chart.Metadata) error { for _, maintainer := range cf.Maintainers { if maintainer == nil { diff --git a/pkg/chart/v2/lint/rules/chartfile_test.go b/pkg/chart/v2/lint/rules/chartfile_test.go index 5a1ad2f24..ddaa72510 100644 --- a/pkg/chart/v2/lint/rules/chartfile_test.go +++ b/pkg/chart/v2/lint/rules/chartfile_test.go @@ -108,6 +108,35 @@ func TestValidateChartVersion(t *testing.T) { } } +func TestValidateChartVersionStrictSemVerV2(t *testing.T) { + var failTest = []struct { + Version string + ErrorMsg string + }{ + {"", "version '' is not a valid SemVerV2"}, + {"1", "version '1' is not a valid SemVerV2"}, + {"1.1", "version '1.1' is not a valid SemVerV2"}, + } + + var successTest = []string{"1.1.1", "0.0.1+build", "0.0.1-beta"} + + for _, test := range failTest { + badChart.Version = test.Version + err := validateChartVersionStrictSemVerV2(badChart) + if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { + t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + } + } + + for _, version := range successTest { + badChart.Version = version + err := validateChartVersionStrictSemVerV2(badChart) + if err != nil { + t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) + } + } +} + func TestValidateChartMaintainer(t *testing.T) { var failTest = []struct { Name string @@ -226,7 +255,7 @@ func TestChartfile(t *testing.T) { linter := support.Linter{ChartDir: badChartDir} Chartfile(&linter) msgs := linter.Messages - expectedNumberOfErrorMessages := 6 + expectedNumberOfErrorMessages := 7 if len(msgs) != expectedNumberOfErrorMessages { t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) @@ -256,13 +285,16 @@ func TestChartfile(t *testing.T) { if !strings.Contains(msgs[5].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { t.Errorf("Unexpected message 5: %s", msgs[5].Err) } + if !strings.Contains(msgs[6].Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") { + t.Errorf("Unexpected message 6: %s", msgs[6].Err) + } }) t.Run("Chart.yaml validity issues due to type mismatch", func(t *testing.T) { linter := support.Linter{ChartDir: anotherBadChartDir} Chartfile(&linter) msgs := linter.Messages - expectedNumberOfErrorMessages := 3 + expectedNumberOfErrorMessages := 4 if len(msgs) != expectedNumberOfErrorMessages { t.Errorf("Expected %d errors, got %d", expectedNumberOfErrorMessages, len(msgs)) @@ -280,5 +312,8 @@ func TestChartfile(t *testing.T) { if !strings.Contains(msgs[2].Err.Error(), "appVersion should be of type string") { t.Errorf("Unexpected message 2: %s", msgs[2].Err) } + if !strings.Contains(msgs[3].Err.Error(), "version '7.2445e+06' is not a valid SemVerV2") { + t.Errorf("Unexpected message 3: %s", msgs[3].Err) + } }) } diff --git a/pkg/chart/v2/metadata.go b/pkg/chart/v2/metadata.go index d213a3491..c46007863 100644 --- a/pkg/chart/v2/metadata.go +++ b/pkg/chart/v2/metadata.go @@ -52,7 +52,7 @@ type Metadata struct { 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. + // A version string of the chart. Required. Version string `json:"version,omitempty"` // A one-sentence description of the chart Description string `json:"description,omitempty"` diff --git a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt index 7b445a69a..67ed58ec3 100644 --- a/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt +++ b/pkg/cmd/testdata/output/lint-chart-with-bad-subcharts-with-subcharts.txt @@ -9,6 +9,7 @@ [ERROR] Chart.yaml: apiVersion is required. The value must be either "v1" or "v2" [ERROR] Chart.yaml: version is required [INFO] Chart.yaml: icon is recommended +[WARNING] Chart.yaml: version '' is not a valid SemVerV2 [WARNING] templates/: directory does not exist [ERROR] : unable to load chart validation: chart.metadata.name is required From cd76ae1c934aeaa22842ef30c898e5888022973b Mon Sep 17 00:00:00 2001 From: Kamil Swiechowski Date: Wed, 3 Sep 2025 08:20:52 +0200 Subject: [PATCH 007/118] feat:strict compliance with semverv2 for chart/v3/linter Signed-off-by: Kamil Swiechowski --- internal/chart/v3/lint/lint_test.go | 2 +- internal/chart/v3/lint/rules/chartfile.go | 4 ++-- internal/chart/v3/lint/rules/chartfile_test.go | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/chart/v3/lint/lint_test.go b/internal/chart/v3/lint/lint_test.go index af44cc58d..6f5912ae7 100644 --- a/internal/chart/v3/lint/lint_test.go +++ b/internal/chart/v3/lint/lint_test.go @@ -60,7 +60,7 @@ func TestBadChartV3(t *testing.T) { } } if msg.Severity == support.ErrorSev { - if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + if strings.Contains(msg.Err.Error(), "version '0.0.0.0' is not a valid SemVerV2") { e = true } if strings.Contains(msg.Err.Error(), "name is required") { diff --git a/internal/chart/v3/lint/rules/chartfile.go b/internal/chart/v3/lint/rules/chartfile.go index e72a0d3b2..fc246ba80 100644 --- a/internal/chart/v3/lint/rules/chartfile.go +++ b/internal/chart/v3/lint/rules/chartfile.go @@ -140,9 +140,9 @@ func validateChartVersion(cf *chart.Metadata) error { return errors.New("version is required") } - version, err := semver.NewVersion(cf.Version) + version, err := semver.StrictNewVersion(cf.Version) if err != nil { - return fmt.Errorf("version '%s' is not a valid SemVer", cf.Version) + return fmt.Errorf("version '%s' is not a valid SemVerV2", cf.Version) } c, err := semver.NewConstraint(">0.0.0-0") diff --git a/internal/chart/v3/lint/rules/chartfile_test.go b/internal/chart/v3/lint/rules/chartfile_test.go index 070cc244d..57893e151 100644 --- a/internal/chart/v3/lint/rules/chartfile_test.go +++ b/internal/chart/v3/lint/rules/chartfile_test.go @@ -84,9 +84,11 @@ func TestValidateChartVersion(t *testing.T) { ErrorMsg string }{ {"", "version is required"}, - {"1.2.3.4", "version '1.2.3.4' is not a valid SemVer"}, - {"waps", "'waps' is not a valid SemVer"}, - {"-3", "'-3' is not a valid SemVer"}, + {"1.2.3.4", "version '1.2.3.4' is not a valid SemVerV2"}, + {"waps", "'waps' is not a valid SemVerV2"}, + {"-3", "'-3' is not a valid SemVerV2"}, + {"1.1", "'1.1' is not a valid SemVerV2"}, + {"1", "'1' is not a valid SemVerV2"}, } var successTest = []string{"0.0.1", "0.0.1+build", "0.0.1-beta"} From 5445b6587b4af85be52e7c92ab3bcfb82cd7652f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:04:14 +0000 Subject: [PATCH 008/118] chore(deps): bump github.com/spf13/pflag from 1.0.7 to 1.0.10 Bumps [github.com/spf13/pflag](https://github.com/spf13/pflag) from 1.0.7 to 1.0.10. - [Release notes](https://github.com/spf13/pflag/releases) - [Commits](https://github.com/spf13/pflag/compare/v1.0.7...v1.0.10) --- updated-dependencies: - dependency-name: github.com/spf13/pflag dependency-version: 1.0.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- go.mod | 2 +- go.sum | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index bbd273413..ca6101583 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( github.com/rubenv/sql-migrate v1.8.0 github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 github.com/spf13/cobra v1.10.1 - github.com/spf13/pflag v1.0.9 + github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.9.0 go.yaml.in/yaml/v3 v3.0.4 diff --git a/go.sum b/go.sum index cca32e249..471125d6f 100644 --- a/go.sum +++ b/go.sum @@ -302,8 +302,9 @@ github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= From 3e97f216cc19bd26ad970a5298d6802ac5fa2ccb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 21:06:24 +0000 Subject: [PATCH 009/118] chore(deps): bump actions/stale from 9.1.0 to 10.0.0 Bumps [actions/stale](https://github.com/actions/stale) from 9.1.0 to 10.0.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/5bef64f19d7facfb25b37b414482c7164d639639...3a9db7e6a41a89f618792c92c0e97cc736e1b13f) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 3417e1734..965410793 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.' From 3d23230ef8fedbc91c6f3c400d1a4bb2d18b632a Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Fri, 5 Sep 2025 10:00:35 +0100 Subject: [PATCH 010/118] chore: add additional logging to plugin installer Signed-off-by: Evans Mungai --- internal/plugin/installer/installer.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index b65dac2f4..a6599c443 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -18,6 +18,7 @@ package installer import ( "errors" "fmt" + "log/slog" "net/http" "os" "path/filepath" @@ -79,6 +80,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error) return nil, err } if _, pathErr := os.Stat(i.Path()); !os.IsNotExist(pathErr) { + slog.Warn("plugin already exists", "path", i.Path(), slog.Any("error", pathErr)) return nil, errors.New("plugin already exists") } @@ -130,6 +132,7 @@ func InstallWithOptions(i Installer, opts Options) (*VerificationResult, error) // Update updates a plugin. func Update(i Installer) error { if _, pathErr := os.Stat(i.Path()); os.IsNotExist(pathErr) { + slog.Warn("plugin does not exist", "path", i.Path(), slog.Any("error", pathErr)) return errors.New("plugin does not exist") } return i.Update() @@ -154,6 +157,7 @@ func NewForSource(source, version string) (Installer, error) { func FindSource(location string) (Installer, error) { installer, err := existingVCSRepo(location) if err != nil && err.Error() == "Cannot detect VCS" { + slog.Warn("cannot get information about plugin source", "location", location, slog.Any("error", err)) return installer, errors.New("cannot get information about plugin source") } return installer, err From a645dfb7f8d1315b500535c1e0fa2b703f097c67 Mon Sep 17 00:00:00 2001 From: Kamil Swiechowski Date: Fri, 5 Sep 2025 13:10:31 +0200 Subject: [PATCH 011/118] fix:semverv2 lint test error message Signed-off-by: Kamil Swiechowski --- pkg/chart/v2/lint/rules/chartfile_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/chart/v2/lint/rules/chartfile_test.go b/pkg/chart/v2/lint/rules/chartfile_test.go index ddaa72510..692358426 100644 --- a/pkg/chart/v2/lint/rules/chartfile_test.go +++ b/pkg/chart/v2/lint/rules/chartfile_test.go @@ -124,7 +124,7 @@ func TestValidateChartVersionStrictSemVerV2(t *testing.T) { badChart.Version = test.Version err := validateChartVersionStrictSemVerV2(badChart) if err == nil || !strings.Contains(err.Error(), test.ErrorMsg) { - t.Errorf("validateChartVersion(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) + t.Errorf("validateChartVersionStrictSemVerV2(%s) to return \"%s\", got no error", test.Version, test.ErrorMsg) } } @@ -132,7 +132,7 @@ func TestValidateChartVersionStrictSemVerV2(t *testing.T) { badChart.Version = version err := validateChartVersionStrictSemVerV2(badChart) if err != nil { - t.Errorf("validateChartVersion(%s) to return no error, got a linter error", version) + t.Errorf("validateChartVersionStrictSemVerV2(%s) to return no error, got a linter error", version) } } } From 9e2d4680e11fc9f2fb209a290e465d56c79e8c72 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Fri, 5 Sep 2025 19:27:44 +0200 Subject: [PATCH 012/118] Warn when we fallback to a different version on `helm pull` Close: https://github.com/helm/helm/issues/31253 Signed-off-by: Benoit Tigeot --- pkg/repo/v1/index.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/repo/v1/index.go b/pkg/repo/v1/index.go index 4de8bb463..d77d70a7f 100644 --- a/pkg/repo/v1/index.go +++ b/pkg/repo/v1/index.go @@ -215,6 +215,7 @@ func (i IndexFile) Get(name, version string) (*ChartVersion, error) { } if constraint.Check(test) { + slog.Warn("unable to find exact version; falling back to closest available version", "chart", name, "requested", version, "selected", ver.Version) return ver, nil } } From b060911075950273c8d42c005dd5928d905c0ab0 Mon Sep 17 00:00:00 2001 From: Jesse Simpson Date: Sat, 6 Sep 2025 11:36:39 -0400 Subject: [PATCH 013/118] refactor: break out into functions and draft tests Signed-off-by: Jesse Simpson --- pkg/engine/engine.go | 61 +++++++++++++++++++++++++++++---------- pkg/engine/engine_test.go | 47 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 15 deletions(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index b42fa3219..a5532f73a 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -360,12 +360,56 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) { // Special case: "template: no template %q associated with template %q" // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 - if strings.HasPrefix(remainder, "no template ") { - return TraceableError{message: s}, true + traceableError, done := parseTemplateNoTemplateError(s, remainder) + if done { + return traceableError, done } // Executing form: ": executing \"\" at <>: [ template:...]" // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 + traceableError, done = parseTemplateExecutingAtErrorType(remainder) + if done { + return traceableError, done + } + + // Simple form: ": " + // Use LastIndex to avoid splitting colons within line:col info. + // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 + traceableError, done = parseTemplateSimpleErrorString(remainder) + if done { + return traceableError, done + } + + return TraceableError{}, false +} + +// Special case: "template: no template %q associated with template %q" +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 +func parseTemplateNoTemplateError(s string, remainder string) (TraceableError, bool) { + if strings.HasPrefix(remainder, "no template ") { + return TraceableError{message: s}, true + } + return TraceableError{}, false +} + +// Simple form: ": " +// Use LastIndex to avoid splitting colons within line:col info. +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 +func parseTemplateSimpleErrorString(remainder string) (TraceableError, bool) { + if sep := strings.LastIndex(remainder, ": "); sep != -1 { + templateName := remainder[:sep] + errMsg := remainder[sep+2:] + if cut := strings.Index(errMsg, " template:"); cut != -1 { + errMsg = errMsg[:cut] + } + return TraceableError{location: templateName, message: errMsg}, true + } + return TraceableError{}, false +} + +// Executing form: ": executing \"\" at <>: [ template:...]" +// Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 +func parseTemplateExecutingAtErrorType(remainder string) (TraceableError, bool) { if idx := strings.Index(remainder, ": executing "); idx != -1 { templateName := remainder[:idx] after := remainder[idx+len(": executing "):] @@ -404,19 +448,6 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) { executedFunction: "executing \"" + functionName + "\" at <" + locationName + ">:", }, true } - - // Simple form: ": " - // Use LastIndex to avoid splitting colons within line:col info. - // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 - if sep := strings.LastIndex(remainder, ": "); sep != -1 { - templateName := remainder[:sep] - errMsg := remainder[sep+2:] - if cut := strings.Index(errMsg, " template:"); cut != -1 { - errMsg = errMsg[:cut] - } - return TraceableError{location: templateName, message: errMsg}, true - } - return TraceableError{}, false } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index f4228fbd7..3362fa37e 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -1428,3 +1428,50 @@ func TestRenderCustomTemplateFuncs(t *testing.T) { t.Errorf("Expected %q, got %q", expected, rendered) } } + +func TestTraceableError_SimpleForm(t *testing.T) { + testStrings := []string{ + "function_not_found/templates/secret.yaml: error calling include", + } + for _, errString := range testStrings { + trace, done := parseTemplateSimpleErrorString(errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.message != "error calling include" { + t.Errorf("Expected %q, got %q", errString, trace.message) + } + } +} +func TestTraceableError_ExecutingForm(t *testing.T) { + testStrings := [][]string{ + {"template: executing \"function_not_found/templates/secret.yaml\" at : ", "function_not_found/templates/secret.yaml"}, + {"template: executing \"name\" at : ", "name"}, + } + for _, errTuple := range testStrings { + errString := errTuple[0] + expectedLocation := errTuple[1] + trace, done := parseTemplateExecutingAtErrorType(errString) + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.location != expectedLocation { + t.Errorf("Expected %q, got %q", expectedLocation, trace.location) + } + } +} + +func TestTraceableError_NoTemplateForm(t *testing.T) { + testStrings := []string{ + "no template \"common.names.get_name\" associated with template \"gotpl\"", + } + for _, errString := range testStrings { + trace, done := parseTemplateNoTemplateError(errString, "") + if !done { + t.Errorf("Expected parse to pass but did not") + } + if trace.message != errString { + t.Errorf("Expected %q, got %q", errString, trace.message) + } + } +} From 712cde46246588beb1fdf2e2119e4faf27a612e4 Mon Sep 17 00:00:00 2001 From: Jesse Simpson Date: Sun, 7 Sep 2025 10:42:24 -0400 Subject: [PATCH 014/118] test: passes now Signed-off-by: Jesse Simpson --- pkg/engine/engine_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 3362fa37e..8dd8f18fe 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -1445,8 +1445,8 @@ func TestTraceableError_SimpleForm(t *testing.T) { } func TestTraceableError_ExecutingForm(t *testing.T) { testStrings := [][]string{ - {"template: executing \"function_not_found/templates/secret.yaml\" at : ", "function_not_found/templates/secret.yaml"}, - {"template: executing \"name\" at : ", "name"}, + {"function_not_found/templates/secret.yaml:6:11: executing \"function_not_found/templates/secret.yaml\" at : ", "function_not_found/templates/secret.yaml:6:11"}, + {"divide_by_zero/templates/secret.yaml:6:11: executing \"divide_by_zero/templates/secret.yaml\" at : ", "divide_by_zero/templates/secret.yaml:6:11"}, } for _, errTuple := range testStrings { errString := errTuple[0] @@ -1466,7 +1466,7 @@ func TestTraceableError_NoTemplateForm(t *testing.T) { "no template \"common.names.get_name\" associated with template \"gotpl\"", } for _, errString := range testStrings { - trace, done := parseTemplateNoTemplateError(errString, "") + trace, done := parseTemplateNoTemplateError(errString, errString) if !done { t.Errorf("Expected parse to pass but did not") } From 939fff9cb97d339ba1c5d2021c424fe73d53ca10 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Sun, 7 Sep 2025 16:45:16 +0200 Subject: [PATCH 015/118] Avoid "panic: interface conversion: interface {} is nil" Closes: #31202 Signed-off-by: Benoit Tigeot --- pkg/chart/common/util/jsonschema.go | 17 ++++- pkg/chart/common/util/jsonschema_test.go | 90 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/pkg/chart/common/util/jsonschema.go b/pkg/chart/common/util/jsonschema.go index acd2ca100..d14435bd8 100644 --- a/pkg/chart/common/util/jsonschema.go +++ b/pkg/chart/common/util/jsonschema.go @@ -93,7 +93,22 @@ func ValidateAgainstSchema(ch chart.Charter, values map[string]interface{}) erro if err != nil { return err } - subchartValues := values[sub.Name()].(map[string]interface{}) + + raw, exists := values[sub.Name()] + if !exists || raw == nil { + // No values provided for this subchart; nothing to validate + continue + } + + subchartValues, ok := raw.(map[string]any) + if !ok { + sb.WriteString(fmt.Sprintf( + "%s:\ninvalid type for values: expected object (map), got %T\n", + sub.Name(), raw, + )) + continue + } + if err := ValidateAgainstSchema(subchart, subchartValues); err != nil { sb.WriteString(err.Error()) } diff --git a/pkg/chart/common/util/jsonschema_test.go b/pkg/chart/common/util/jsonschema_test.go index b34f9d514..6fec260ab 100644 --- a/pkg/chart/common/util/jsonschema_test.go +++ b/pkg/chart/common/util/jsonschema_test.go @@ -286,3 +286,93 @@ func TestHTTPURLLoader_Load(t *testing.T) { } }) } + +// Non-regression tests for https://github.com/helm/helm/issues/31202 +// Ensure ValidateAgainstSchema does not panic when: +// - subchart key is missing +// - subchart value is nil +// - subchart value has an invalid type + +func TestValidateAgainstSchema_MissingSubchartValues_NoPanic(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "subchart"}, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{Name: "chrt"}, + } + chrt.AddDependency(subchart) + + // No "subchart" key present in values + vals := map[string]any{ + "name": "John", + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("ValidateAgainstSchema panicked (missing subchart values): %v", r) + } + }() + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Fatalf("expected no error when subchart values are missing, got: %v", err) + } +} + +func TestValidateAgainstSchema_SubchartNil_NoPanic(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "subchart"}, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{Name: "chrt"}, + } + chrt.AddDependency(subchart) + + // "subchart" key present but nil + vals := map[string]any{ + "name": "John", + "subchart": nil, + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("ValidateAgainstSchema panicked (nil subchart values): %v", r) + } + }() + + if err := ValidateAgainstSchema(chrt, vals); err != nil { + t.Fatalf("expected no error when subchart values are nil, got: %v", err) + } +} + +func TestValidateAgainstSchema_InvalidSubchartValuesType_NoPanic(t *testing.T) { + subchartJSON := []byte(subchartSchema) + subchart := &chart.Chart{ + Metadata: &chart.Metadata{Name: "subchart"}, + Schema: subchartJSON, + } + chrt := &chart.Chart{ + Metadata: &chart.Metadata{Name: "chrt"}, + } + chrt.AddDependency(subchart) + + // "subchart" is the wrong type (string instead of map) + vals := map[string]any{ + "name": "John", + "subchart": "oops", + } + + defer func() { + if r := recover(); r != nil { + t.Fatalf("ValidateAgainstSchema panicked (invalid subchart values type): %v", r) + } + }() + + // We expect a non-nil error (invalid type), but crucially no panic. + if err := ValidateAgainstSchema(chrt, vals); err == nil { + t.Fatalf("expected an error when subchart values have invalid type, got nil") + } +} From 1904ef6ad87c43cb0190ec45e3cf1cd03c7bdea8 Mon Sep 17 00:00:00 2001 From: Stephanie Hohenberg Date: Sun, 7 Sep 2025 11:01:16 -0400 Subject: [PATCH 016/118] Adapt test-coverage command to be able to run for a certain package Signed-off-by: Stephanie Hohenberg --- Makefile | 9 +++------ scripts/coverage.sh | 3 ++- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 5e1bfc6c2..e3e6cb538 100644 --- a/Makefile +++ b/Makefile @@ -118,11 +118,12 @@ test-unit: go test $(GOFLAGS) -run ^TestHelmCreateChart_CheckDeprecatedWarnings$$ ./internal/chart/v3/lint/ $(TESTFLAGS) -ldflags '$(LDFLAGS)' +# To run the coverage for a specific package use: make test-coverage PKG=./pkg/action .PHONY: test-coverage test-coverage: @echo - @echo "==> Running unit tests with coverage <==" - @ ./scripts/coverage.sh + @echo "==> Running unit tests with coverage: $(PKG) <==" + @ ./scripts/coverage.sh $(PKG) .PHONY: test-style test-style: @@ -148,10 +149,6 @@ test-acceptance: build build-cross test-acceptance-completion: ACCEPTANCE_RUN_TESTS = shells.robot test-acceptance-completion: test-acceptance -.PHONY: coverage -coverage: - @scripts/coverage.sh - .PHONY: format format: $(GOIMPORTS) go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w -local helm.sh/helm diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 2164d94da..487d4eeee 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -19,9 +19,10 @@ set -euo pipefail covermode=${COVERMODE:-atomic} coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) profile="${coverdir}/cover.out" +target="${1:-./...}" # by default the whole repository is tested generate_cover_data() { - for d in $(go list ./...) ; do + for d in $(go list "$target"); do ( local output="${coverdir}/${d//\//-}.cover" go test -coverprofile="${output}" -covermode="$covermode" "$d" From e19d9fb6eec19f09e8fecb548b4674195221f3db Mon Sep 17 00:00:00 2001 From: Stephanie Hohenberg Date: Sun, 7 Sep 2025 10:50:02 -0400 Subject: [PATCH 017/118] Refactor unreachableKubeClient for testing into failingKubeClient Signed-off-by: Stephanie Hohenberg --- pkg/action/get_metadata_test.go | 15 +++------------ pkg/action/get_values_test.go | 7 ++++--- pkg/action/install_test.go | 16 ++++++++++++++++ pkg/action/list_test.go | 16 ++++++++++++++++ pkg/action/uninstall_test.go | 16 ++++++++++++++++ pkg/action/upgrade_test.go | 17 +++++++++++++++++ .../fake/{fake.go => failing_kube_client.go} | 8 ++++++++ 7 files changed, 80 insertions(+), 15 deletions(-) rename pkg/kube/fake/{fake.go => failing_kube_client.go} (96%) diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index 6ceb34951..ca612fed7 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -31,15 +31,6 @@ import ( helmtime "helm.sh/helm/v4/pkg/time" ) -// unreachableKubeClient is a test client that always returns an error for IsReachable -type unreachableKubeClient struct { - kubefake.PrintingKubeClient -} - -func (u *unreachableKubeClient) IsReachable() error { - return errors.New("connection refused") -} - func TestNewGetMetadata(t *testing.T) { cfg := actionConfigFixture(t) client := NewGetMetadata(cfg) @@ -424,9 +415,9 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) { func TestGetMetadata_Run_UnreachableKubeClient(t *testing.T) { cfg := actionConfigFixture(t) - cfg.KubeClient = &unreachableKubeClient{ - PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, - } + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + cfg.KubeClient = &failingKubeClient client := NewGetMetadata(cfg) diff --git a/pkg/action/get_values_test.go b/pkg/action/get_values_test.go index ec785b5c7..b8630c322 100644 --- a/pkg/action/get_values_test.go +++ b/pkg/action/get_values_test.go @@ -17,6 +17,7 @@ limitations under the License. package action import ( + "errors" "io" "testing" @@ -168,9 +169,9 @@ func TestGetValues_Run_EmptyValues(t *testing.T) { func TestGetValues_Run_UnreachableKubeClient(t *testing.T) { cfg := actionConfigFixture(t) - cfg.KubeClient = &unreachableKubeClient{ - PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, - } + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + cfg.KubeClient = &failingKubeClient client := NewGetValues(cfg) diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 92bb64b4d..4c4890ec9 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -997,3 +997,19 @@ func TestUrlEqual(t *testing.T) { }) } } + +func TestInstallRun_UnreachableKubeClient(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + instAction := NewInstall(config) + instAction.ClientOnly = false + ctx, done := context.WithCancel(t.Context()) + res, err := instAction.RunWithContext(ctx, nil, nil) + + done() + assert.Nil(t, res) + assert.ErrorContains(t, err, "connection refused") +} diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index b6f89fa1e..75737d635 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -17,10 +17,13 @@ limitations under the License. package action import ( + "errors" + "io" "testing" "github.com/stretchr/testify/assert" + kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" ) @@ -367,3 +370,16 @@ func TestSelectorList(t *testing.T) { assert.ElementsMatch(t, expectedFilteredList, res) }) } + +func TestListRun_UnreachableKubeClient(t *testing.T) { + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + lister := NewList(config) + result, err := lister.Run() + + assert.Nil(t, result) + assert.ErrorContains(t, err, "connection refused") +} diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index f7c9e5f44..7c7344383 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -17,7 +17,9 @@ limitations under the License. package action import ( + "errors" "fmt" + "io" "testing" "github.com/stretchr/testify/assert" @@ -151,3 +153,17 @@ func TestUninstallRelease_Cascade(t *testing.T) { require.Error(t, err) is.Contains(err.Error(), "failed to delete release: come-fail-away") } + +func TestUninstallRun_UnreachableKubeClient(t *testing.T) { + t.Helper() + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + client := NewUninstall(config) + result, err := client.Run("") + + assert.Nil(t, result) + assert.ErrorContains(t, err, "connection refused") +} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 7e27ef594..d31804b87 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -18,7 +18,9 @@ package action import ( "context" + "errors" "fmt" + "io" "reflect" "testing" "time" @@ -690,3 +692,18 @@ func TestGetUpgradeServerSideValue(t *testing.T) { } } + +func TestUpgradeRun_UnreachableKubeClient(t *testing.T) { + t.Helper() + config := actionConfigFixture(t) + failingKubeClient := kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}, DummyResources: nil} + failingKubeClient.ConnectionError = errors.New("connection refused") + config.KubeClient = &failingKubeClient + + client := NewUpgrade(config) + vals := map[string]interface{}{} + result, err := client.Run("", buildChart(), vals) + + assert.Nil(t, result) + assert.ErrorContains(t, err, "connection refused") +} diff --git a/pkg/kube/fake/fake.go b/pkg/kube/fake/failing_kube_client.go similarity index 96% rename from pkg/kube/fake/fake.go rename to pkg/kube/fake/failing_kube_client.go index 588bba83d..154419ebf 100644 --- a/pkg/kube/fake/fake.go +++ b/pkg/kube/fake/failing_kube_client.go @@ -40,6 +40,7 @@ type FailingKubeClient struct { UpdateError error BuildError error BuildTableError error + ConnectionError error BuildDummy bool DummyResources kube.ResourceList BuildUnstructuredError error @@ -166,6 +167,13 @@ func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) }, nil } +func (f *FailingKubeClient) IsReachable() error { + if f.ConnectionError != nil { + return f.ConnectionError + } + return f.PrintingKubeClient.IsReachable() +} + func createDummyResourceList() kube.ResourceList { var resInfo resource.Info resInfo.Name = "dummyName" From fae2736d1b56377aa9cde8f30fc1ed13a141f728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:27:19 +0000 Subject: [PATCH 018/118] chore(deps): bump golang.org/x/term from 0.34.0 to 0.35.0 Bumps [golang.org/x/term](https://github.com/golang/term) from 0.34.0 to 0.35.0. - [Commits](https://github.com/golang/term/compare/v0.34.0...v0.35.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-version: 0.35.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index bbd273413..5852cf251 100644 --- a/go.mod +++ b/go.mod @@ -36,7 +36,7 @@ require ( github.com/tetratelabs/wazero v1.9.0 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.41.0 - golang.org/x/term v0.34.0 + golang.org/x/term v0.35.0 golang.org/x/text v0.28.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.0 @@ -164,7 +164,7 @@ require ( golang.org/x/net v0.42.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect + golang.org/x/sys v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect diff --git a/go.sum b/go.sum index cca32e249..c8bdfc7b8 100644 --- a/go.sum +++ b/go.sum @@ -449,8 +449,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -458,8 +458,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From 86c19fdc2a8daf3e4fe5a3748a590f9568598560 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 21:29:27 +0000 Subject: [PATCH 019/118] chore(deps): bump sigs.k8s.io/controller-runtime from 0.22.0 to 0.22.1 Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.22.0 to 0.22.1. - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.22.0...v0.22.1) --- updated-dependencies: - dependency-name: sigs.k8s.io/controller-runtime dependency-version: 0.22.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 bbd273413..a666b19a3 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.34.0 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.22.0 + sigs.k8s.io/controller-runtime v0.22.1 sigs.k8s.io/kustomize/kyaml v0.20.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index cca32e249..25abf860c 100644 --- a/go.sum +++ b/go.sum @@ -531,8 +531,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.22.0 h1:mTOfibb8Hxwpx3xEkR56i7xSjB+nH4hZG37SrlCY5e0= -sigs.k8s.io/controller-runtime v0.22.0/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= +sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= From b539309aa226e92194d6c63533e682524b31b51b Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 9 Sep 2025 12:52:31 -0600 Subject: [PATCH 020/118] fix: idea gitignore entry Signed-off-by: Terry Howe --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ea0717ed..0fd2c6bda 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.swp .DS_Store .coverage/ -.idea/ +.idea .vimrc .vscode/ .devcontainer/ From 62cd5d8ba8ece180a75cf8abab3877398efec36c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:14:00 +0000 Subject: [PATCH 021/118] chore(deps): bump golang.org/x/crypto from 0.41.0 to 0.42.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.41.0 to 0.42.0. - [Commits](https://github.com/golang/crypto/compare/v0.41.0...v0.42.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.42.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 12 ++++++------ go.sum | 24 ++++++++++++------------ 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 735bd66bd..6b11a9902 100644 --- a/go.mod +++ b/go.mod @@ -35,9 +35,9 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.9.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.41.0 + golang.org/x/crypto v0.42.0 golang.org/x/term v0.35.0 - golang.org/x/text v0.28.0 + golang.org/x/text v0.29.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.0 k8s.io/apiextensions-apiserver v0.34.0 @@ -160,13 +160,13 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/mod v0.26.0 // indirect - golang.org/x/net v0.42.0 // indirect + golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.16.0 // indirect + golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/grpc v1.72.1 // indirect diff --git a/go.sum b/go.sum index 6bd7906f4..0039f5769 100644 --- a/go.sum +++ b/go.sum @@ -391,16 +391,16 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= -golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= +golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -414,8 +414,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= -golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -428,8 +428,8 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -467,8 +467,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -479,8 +479,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= -golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 072e2a689ad8ed72258f851e23c5d987f7979ffe Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 10 Sep 2025 15:19:17 +0200 Subject: [PATCH 022/118] Extend --skip-schema-validation for lint command When --skip-schema-validation is enabled, the lint command will now skip JSON schema validation for values.yaml files, allowing charts with schema validation errors to pass linting when the flag is used. This addresses the gap where --skip-schema-validation only applied to templates but not to values files, providing complete schema validation bypass when needed. Fixes: #13413 Signed-off-by: Suleiman Dibirov Signed-off-by: Benoit Tigeot --- internal/chart/v3/lint/lint.go | 2 +- internal/chart/v3/lint/rules/values.go | 13 +++++++---- internal/chart/v3/lint/rules/values_test.go | 24 ++++++++++++++++----- pkg/chart/v2/lint/lint.go | 2 +- pkg/chart/v2/lint/rules/values.go | 13 +++++++---- pkg/chart/v2/lint/rules/values_test.go | 24 ++++++++++++++++----- 6 files changed, 58 insertions(+), 20 deletions(-) diff --git a/internal/chart/v3/lint/lint.go b/internal/chart/v3/lint/lint.go index 231bb6803..0cd949065 100644 --- a/internal/chart/v3/lint/lint.go +++ b/internal/chart/v3/lint/lint.go @@ -57,7 +57,7 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt } rules.Chartfile(&result) - rules.ValuesWithOverrides(&result, values) + rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation) rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) rules.Dependencies(&result) rules.Crds(&result) diff --git a/internal/chart/v3/lint/rules/values.go b/internal/chart/v3/lint/rules/values.go index adf2e2c52..0af9765dd 100644 --- a/internal/chart/v3/lint/rules/values.go +++ b/internal/chart/v3/lint/rules/values.go @@ -32,7 +32,7 @@ import ( // they are only tested for well-formedness. // // If additional values are supplied, they are coalesced into the values in values.yaml. -func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) { +func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) { file := "values.yaml" vf := filepath.Join(linter.ChartDir, file) fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) @@ -41,7 +41,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]inter return } - linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides)) + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation)) } func validateValuesFileExistence(valuesPath string) error { @@ -52,7 +52,7 @@ func validateValuesFileExistence(valuesPath string) error { return nil } -func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { +func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error { values, err := common.ReadValuesFile(valuesPath) if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) @@ -75,5 +75,10 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err if err != nil { return err } - return util.ValidateAgainstSingleSchema(coalescedValues, schema) + + if !skipSchemaValidation { + return util.ValidateAgainstSingleSchema(coalescedValues, schema) + } + + return nil } diff --git a/internal/chart/v3/lint/rules/values_test.go b/internal/chart/v3/lint/rules/values_test.go index 348695785..288b77436 100644 --- a/internal/chart/v3/lint/rules/values_test.go +++ b/internal/chart/v3/lint/rules/values_test.go @@ -67,7 +67,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) { ` tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil { + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil { t.Fatal("expected values file to fail parsing") } } @@ -78,7 +78,7 @@ func TestValidateValuesFileSchema(t *testing.T) { createTestingSchema(t, tmpdir) valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil { + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -91,7 +91,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { valfile := filepath.Join(tmpdir, "values.yaml") - err := validateValuesFile(valfile, map[string]interface{}{}) + err := validateValuesFile(valfile, map[string]interface{}{}, false) if err == nil { t.Fatal("expected values file to fail parsing") } @@ -99,6 +99,20 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { assert.Contains(t, err.Error(), "- at '/username': got number, want string") } +func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) { + // 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}, true) + if err != nil { + t.Fatal("expected values file to pass parsing because of skipSchemaValidation") + } +} + func TestValidateValuesFileSchemaOverrides(t *testing.T) { yaml := "username: admin" overrides := map[string]interface{}{ @@ -108,7 +122,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) { createTestingSchema(t, tmpdir) valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, overrides); err != nil { + if err := validateValuesFile(valfile, overrides, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -145,7 +159,7 @@ func TestValidateValuesFile(t *testing.T) { valfile := filepath.Join(tmpdir, "values.yaml") - err := validateValuesFile(valfile, tt.overrides) + err := validateValuesFile(valfile, tt.overrides, false) switch { case err != nil && tt.errorMessage == "": diff --git a/pkg/chart/v2/lint/lint.go b/pkg/chart/v2/lint/lint.go index 773c9bc5e..b26d65a34 100644 --- a/pkg/chart/v2/lint/lint.go +++ b/pkg/chart/v2/lint/lint.go @@ -57,7 +57,7 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt } rules.Chartfile(&result) - rules.ValuesWithOverrides(&result, values) + rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation) rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) rules.Dependencies(&result) rules.Crds(&result) diff --git a/pkg/chart/v2/lint/rules/values.go b/pkg/chart/v2/lint/rules/values.go index 5260bf8b3..994a6a463 100644 --- a/pkg/chart/v2/lint/rules/values.go +++ b/pkg/chart/v2/lint/rules/values.go @@ -32,7 +32,7 @@ import ( // they are only tested for well-formedness. // // If additional values are supplied, they are coalesced into the values in values.yaml. -func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}) { +func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]interface{}, skipSchemaValidation bool) { file := "values.yaml" vf := filepath.Join(linter.ChartDir, file) fileExists := linter.RunLinterRule(support.InfoSev, file, validateValuesFileExistence(vf)) @@ -41,7 +41,7 @@ func ValuesWithOverrides(linter *support.Linter, valueOverrides map[string]inter return } - linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides)) + linter.RunLinterRule(support.ErrorSev, file, validateValuesFile(vf, valueOverrides, skipSchemaValidation)) } func validateValuesFileExistence(valuesPath string) error { @@ -52,7 +52,7 @@ func validateValuesFileExistence(valuesPath string) error { return nil } -func validateValuesFile(valuesPath string, overrides map[string]interface{}) error { +func validateValuesFile(valuesPath string, overrides map[string]interface{}, skipSchemaValidation bool) error { values, err := common.ReadValuesFile(valuesPath) if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) @@ -75,5 +75,10 @@ func validateValuesFile(valuesPath string, overrides map[string]interface{}) err if err != nil { return err } - return util.ValidateAgainstSingleSchema(coalescedValues, schema) + + if !skipSchemaValidation { + return util.ValidateAgainstSingleSchema(coalescedValues, schema) + } + + return nil } diff --git a/pkg/chart/v2/lint/rules/values_test.go b/pkg/chart/v2/lint/rules/values_test.go index 348695785..288b77436 100644 --- a/pkg/chart/v2/lint/rules/values_test.go +++ b/pkg/chart/v2/lint/rules/values_test.go @@ -67,7 +67,7 @@ func TestValidateValuesFileWellFormed(t *testing.T) { ` tmpdir := ensure.TempFile(t, "values.yaml", []byte(badYaml)) valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]interface{}{}); err == nil { + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err == nil { t.Fatal("expected values file to fail parsing") } } @@ -78,7 +78,7 @@ func TestValidateValuesFileSchema(t *testing.T) { createTestingSchema(t, tmpdir) valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, map[string]interface{}{}); err != nil { + if err := validateValuesFile(valfile, map[string]interface{}{}, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -91,7 +91,7 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { valfile := filepath.Join(tmpdir, "values.yaml") - err := validateValuesFile(valfile, map[string]interface{}{}) + err := validateValuesFile(valfile, map[string]interface{}{}, false) if err == nil { t.Fatal("expected values file to fail parsing") } @@ -99,6 +99,20 @@ func TestValidateValuesFileSchemaFailure(t *testing.T) { assert.Contains(t, err.Error(), "- at '/username': got number, want string") } +func TestValidateValuesFileSchemaFailureButWithSkipSchemaValidation(t *testing.T) { + // 1234 is an int, not a string. This should fail normally but pass with skipSchemaValidation. + yaml := "username: 1234\npassword: swordfish" + tmpdir := ensure.TempFile(t, "values.yaml", []byte(yaml)) + createTestingSchema(t, tmpdir) + + valfile := filepath.Join(tmpdir, "values.yaml") + + err := validateValuesFile(valfile, map[string]interface{}{}, true) + if err != nil { + t.Fatal("expected values file to pass parsing because of skipSchemaValidation") + } +} + func TestValidateValuesFileSchemaOverrides(t *testing.T) { yaml := "username: admin" overrides := map[string]interface{}{ @@ -108,7 +122,7 @@ func TestValidateValuesFileSchemaOverrides(t *testing.T) { createTestingSchema(t, tmpdir) valfile := filepath.Join(tmpdir, "values.yaml") - if err := validateValuesFile(valfile, overrides); err != nil { + if err := validateValuesFile(valfile, overrides, false); err != nil { t.Fatalf("Failed validation with %s", err) } } @@ -145,7 +159,7 @@ func TestValidateValuesFile(t *testing.T) { valfile := filepath.Join(tmpdir, "values.yaml") - err := validateValuesFile(valfile, tt.overrides) + err := validateValuesFile(valfile, tt.overrides, false) switch { case err != nil && tt.errorMessage == "": From 99e5fce71a51a948bcfde040cd9be77e02a431f2 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 10 Sep 2025 21:26:02 +0200 Subject: [PATCH 023/118] Fix deprecation warning for spf13/pflag from 1.0.7 to 1.0.10 Close: #31231 ``` Error: cmd/helm/root.go:165:2: SA1019: flags.ParseErrorsWhitelist is deprecated: use [FlagSet.ParseErrorsAllowlist] instead. This field will be removed in a future release. (staticcheck) ``` Signed-off-by: Benoit Tigeot --- pkg/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 4753e51fe..4f1be88d6 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -173,7 +173,7 @@ func newRootCmdWithConfig(actionConfig *action.Configuration, out io.Writer, arg // those errors will be caught later during the call to cmd.Execution. // This call is required to gather configuration information prior to // execution. - flags.ParseErrorsWhitelist.UnknownFlags = true + flags.ParseErrorsAllowlist.UnknownFlags = true flags.Parse(args) logSetup(settings.Debug) From 20f4e1f12de3ddca906265b05a98fb5be2cd2122 Mon Sep 17 00:00:00 2001 From: Evan Anderson Date: Tue, 2 Sep 2025 20:20:09 -0700 Subject: [PATCH 024/118] Use filepath.Path when handling directory names Signed-off-by: Evan Anderson --- pkg/action/install.go | 3 +-- pkg/cmd/template.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index 0fe3ebc4b..79acc8c7d 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -26,7 +26,6 @@ import ( "log/slog" "net/url" "os" - "path" "path/filepath" "strings" "sync" @@ -677,7 +676,7 @@ func createOrOpenFile(filename string, appendData bool) (*os.File, error) { // check if the directory exists to create file. creates if doesn't exist func ensureDirectoryForFile(file string) error { - baseDir := path.Dir(file) + baseDir := filepath.Dir(file) _, err := os.Stat(baseDir) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 81c112d51..59c24f39e 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -23,7 +23,6 @@ import ( "io" "io/fs" "os" - "path" "path/filepath" "regexp" "slices" @@ -250,7 +249,7 @@ func createOrOpenFile(filename string, appendData bool) (*os.File, error) { } func ensureDirectoryForFile(file string) error { - baseDir := path.Dir(file) + baseDir := filepath.Dir(file) _, err := os.Stat(baseDir) if err != nil && !errors.Is(err, fs.ErrNotExist) { return err From fcd082e03417d78dec8c105254cd967df8277f48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:12:10 +0000 Subject: [PATCH 025/118] chore(deps): bump the k8s-io group with 7 updates Bumps the k8s-io group with 7 updates: | Package | From | To | | --- | --- | --- | | [k8s.io/api](https://github.com/kubernetes/api) | `0.34.0` | `0.34.1` | | [k8s.io/apiextensions-apiserver](https://github.com/kubernetes/apiextensions-apiserver) | `0.34.0` | `0.34.1` | | [k8s.io/apimachinery](https://github.com/kubernetes/apimachinery) | `0.34.0` | `0.34.1` | | [k8s.io/apiserver](https://github.com/kubernetes/apiserver) | `0.34.0` | `0.34.1` | | [k8s.io/cli-runtime](https://github.com/kubernetes/cli-runtime) | `0.34.0` | `0.34.1` | | [k8s.io/client-go](https://github.com/kubernetes/client-go) | `0.34.0` | `0.34.1` | | [k8s.io/kubectl](https://github.com/kubernetes/kubectl) | `0.34.0` | `0.34.1` | Updates `k8s.io/api` from 0.34.0 to 0.34.1 - [Commits](https://github.com/kubernetes/api/compare/v0.34.0...v0.34.1) Updates `k8s.io/apiextensions-apiserver` from 0.34.0 to 0.34.1 - [Release notes](https://github.com/kubernetes/apiextensions-apiserver/releases) - [Commits](https://github.com/kubernetes/apiextensions-apiserver/compare/v0.34.0...v0.34.1) Updates `k8s.io/apimachinery` from 0.34.0 to 0.34.1 - [Commits](https://github.com/kubernetes/apimachinery/compare/v0.34.0...v0.34.1) Updates `k8s.io/apiserver` from 0.34.0 to 0.34.1 - [Commits](https://github.com/kubernetes/apiserver/compare/v0.34.0...v0.34.1) Updates `k8s.io/cli-runtime` from 0.34.0 to 0.34.1 - [Commits](https://github.com/kubernetes/cli-runtime/compare/v0.34.0...v0.34.1) Updates `k8s.io/client-go` from 0.34.0 to 0.34.1 - [Changelog](https://github.com/kubernetes/client-go/blob/master/CHANGELOG.md) - [Commits](https://github.com/kubernetes/client-go/compare/v0.34.0...v0.34.1) Updates `k8s.io/kubectl` from 0.34.0 to 0.34.1 - [Commits](https://github.com/kubernetes/kubectl/compare/v0.34.0...v0.34.1) --- updated-dependencies: - dependency-name: k8s.io/api dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apiextensions-apiserver dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apimachinery dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/apiserver dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/cli-runtime dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/client-go dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io - dependency-name: k8s.io/kubectl dependency-version: 0.34.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: k8s-io ... Signed-off-by: dependabot[bot] --- go.mod | 16 ++++++++-------- go.sum | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 5a69e5fd6..77e761de2 100644 --- a/go.mod +++ b/go.mod @@ -39,14 +39,14 @@ require ( golang.org/x/term v0.35.0 golang.org/x/text v0.29.0 gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.34.0 - k8s.io/apiextensions-apiserver v0.34.0 - k8s.io/apimachinery v0.34.0 - k8s.io/apiserver v0.34.0 - k8s.io/cli-runtime v0.34.0 - k8s.io/client-go v0.34.0 + k8s.io/api v0.34.1 + k8s.io/apiextensions-apiserver v0.34.1 + k8s.io/apimachinery v0.34.1 + k8s.io/apiserver v0.34.1 + k8s.io/cli-runtime v0.34.1 + k8s.io/client-go v0.34.1 k8s.io/klog/v2 v2.130.1 - k8s.io/kubectl v0.34.0 + k8s.io/kubectl v0.34.1 oras.land/oras-go/v2 v2.6.0 sigs.k8s.io/controller-runtime v0.22.1 sigs.k8s.io/kustomize/kyaml v0.20.1 @@ -174,7 +174,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 - k8s.io/component-base v0.34.0 // indirect + k8s.io/component-base v0.34.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect diff --git a/go.sum b/go.sum index f6086fc2e..9fa40e4d4 100644 --- a/go.sum +++ b/go.sum @@ -508,26 +508,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.34.0 h1:L+JtP2wDbEYPUeNGbeSa/5GwFtIA662EmT2YSLOkAVE= -k8s.io/api v0.34.0/go.mod h1:YzgkIzOOlhl9uwWCZNqpw6RJy9L2FK4dlJeayUoydug= -k8s.io/apiextensions-apiserver v0.34.0 h1:B3hiB32jV7BcyKcMU5fDaDxk882YrJ1KU+ZSkA9Qxoc= -k8s.io/apiextensions-apiserver v0.34.0/go.mod h1:hLI4GxE1BDBy9adJKxUxCEHBGZtGfIg98Q+JmTD7+g0= -k8s.io/apimachinery v0.34.0 h1:eR1WO5fo0HyoQZt1wdISpFDffnWOvFLOOeJ7MgIv4z0= -k8s.io/apimachinery v0.34.0/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= -k8s.io/apiserver v0.34.0 h1:Z51fw1iGMqN7uJ1kEaynf2Aec1Y774PqU+FVWCFV3Jg= -k8s.io/apiserver v0.34.0/go.mod h1:52ti5YhxAvewmmpVRqlASvaqxt0gKJxvCeW7ZrwgazQ= -k8s.io/cli-runtime v0.34.0 h1:N2/rUlJg6TMEBgtQ3SDRJwa8XyKUizwjlOknT1mB2Cw= -k8s.io/cli-runtime v0.34.0/go.mod h1:t/skRecS73Piv+J+FmWIQA2N2/rDjdYSQzEE67LUUs8= -k8s.io/client-go v0.34.0 h1:YoWv5r7bsBfb0Hs2jh8SOvFbKzzxyNo0nSb0zC19KZo= -k8s.io/client-go v0.34.0/go.mod h1:ozgMnEKXkRjeMvBZdV1AijMHLTh3pbACPvK7zFR+QQY= -k8s.io/component-base v0.34.0 h1:bS8Ua3zlJzapklsB1dZgjEJuJEeHjj8yTu1gxE2zQX8= -k8s.io/component-base v0.34.0/go.mod h1:RSCqUdvIjjrEm81epPcjQ/DS+49fADvGSCkIP3IC6vg= +k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= +k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= +k8s.io/apiextensions-apiserver v0.34.1 h1:NNPBva8FNAPt1iSVwIE0FsdrVriRXMsaWFMqJbII2CI= +k8s.io/apiextensions-apiserver v0.34.1/go.mod h1:hP9Rld3zF5Ay2Of3BeEpLAToP+l4s5UlxiHfqRaRcMc= +k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= +k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/apiserver v0.34.1 h1:U3JBGdgANK3dfFcyknWde1G6X1F4bg7PXuvlqt8lITA= +k8s.io/apiserver v0.34.1/go.mod h1:eOOc9nrVqlBI1AFCvVzsob0OxtPZUCPiUJL45JOTBG0= +k8s.io/cli-runtime v0.34.1 h1:btlgAgTrYd4sk8vJTRG6zVtqBKt9ZMDeQZo2PIzbL7M= +k8s.io/cli-runtime v0.34.1/go.mod h1:aVA65c+f0MZiMUPbseU/M9l1Wo2byeaGwUuQEQVVveE= +k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= +k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= +k8s.io/component-base v0.34.1 h1:v7xFgG+ONhytZNFpIz5/kecwD+sUhVE6HU7qQUiRM4A= +k8s.io/component-base v0.34.1/go.mod h1:mknCpLlTSKHzAQJJnnHVKqjxR7gBeHRv0rPXA7gdtQ0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/kubectl v0.34.0 h1:NcXz4TPTaUwhiX4LU+6r6udrlm0NsVnSkP3R9t0dmxs= -k8s.io/kubectl v0.34.0/go.mod h1:bmd0W5i+HuG7/p5sqicr0Li0rR2iIhXL0oUyLF3OjR4= +k8s.io/kubectl v0.34.1 h1:1qP1oqT5Xc93K+H8J7ecpBjaz511gan89KO9Vbsh/OI= +k8s.io/kubectl v0.34.1/go.mod h1:JRYlhJpGPyk3dEmJ+BuBiOB9/dAvnrALJEiY/C5qa6A= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= From db50c37eda4ccb5d07bbd6733fd27926346f551f Mon Sep 17 00:00:00 2001 From: bennsimon Date: Fri, 12 Sep 2025 12:44:18 +0300 Subject: [PATCH 026/118] remove metadata output on helm template Signed-off-by: bennsimon --- pkg/engine/engine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index a0ca17f08..7c858690f 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -473,7 +473,6 @@ func recAllTpls(c ci.Charter, templates map[string]renderable, values common.Val slog.Error("error accessing chart", "error", err) } chartMetaData := accessor.MetadataAsMap() - fmt.Printf("metadata: %v\n", chartMetaData) chartMetaData["IsRoot"] = accessor.IsRoot() next := map[string]interface{}{ From cfaf30083af5b32ae4611c7e58d8dbc2171e9331 Mon Sep 17 00:00:00 2001 From: yajianggroup Date: Fri, 12 Sep 2025 19:03:54 +0800 Subject: [PATCH 027/118] refactor: use strings.CutPrefix Signed-off-by: yajianggroup --- internal/plugin/installer/extractor.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/plugin/installer/extractor.go b/internal/plugin/installer/extractor.go index 9417a0535..407138197 100644 --- a/internal/plugin/installer/extractor.go +++ b/internal/plugin/installer/extractor.go @@ -185,8 +185,8 @@ func (g *TarGzExtractor) Extract(buffer *bytes.Buffer, targetDir string) error { func stripPluginName(name string) string { var strippedName string for suffix := range Extractors { - if strings.HasSuffix(name, suffix) { - strippedName = strings.TrimSuffix(name, suffix) + if before, ok := strings.CutSuffix(name, suffix); ok { + strippedName = before break } } From 934b4550d9f238b2b9537ea0ea14207d1d1f6ade Mon Sep 17 00:00:00 2001 From: Mile Druzijanic Date: Fri, 12 Sep 2025 20:42:37 +0200 Subject: [PATCH 028/118] improve fileutil test coverage Signed-off-by: Mile Druzijanic --- internal/fileutil/fileutil_test.go | 64 ++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/internal/fileutil/fileutil_test.go b/internal/fileutil/fileutil_test.go index 92920d3c4..881fbb49d 100644 --- a/internal/fileutil/fileutil_test.go +++ b/internal/fileutil/fileutil_test.go @@ -20,9 +20,12 @@ import ( "bytes" "os" "path/filepath" + "strings" "testing" ) +// TestAtomicWriteFile tests the happy path of AtomicWriteFile function. +// It verifies that the function correctly writes content to a file with the specified mode. func TestAtomicWriteFile(t *testing.T) { dir := t.TempDir() @@ -55,3 +58,64 @@ func TestAtomicWriteFile(t *testing.T) { mode, gotinfo.Mode()) } } + +// TestAtomicWriteFile_CreateTempError tests the error path when os.CreateTemp fails +func TestAtomicWriteFile_CreateTempError(t *testing.T) { + invalidPath := "/invalid/path/that/does/not/exist/testfile" + + reader := bytes.NewReader([]byte("test content")) + mode := os.FileMode(0644) + + err := AtomicWriteFile(invalidPath, reader, mode) + if err == nil { + t.Error("Expected error when CreateTemp fails, but got nil") + } +} + +// TestAtomicWriteFile_EmptyContent tests with empty content +func TestAtomicWriteFile_EmptyContent(t *testing.T) { + dir := t.TempDir() + testpath := filepath.Join(dir, "empty_helm") + + reader := bytes.NewReader([]byte("")) + mode := os.FileMode(0644) + + err := AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error with empty content: %s", err) + } + + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if len(got) != 0 { + t.Fatalf("expected empty content, got: %s", string(got)) + } +} + +// TestAtomicWriteFile_LargeContent tests with large content +func TestAtomicWriteFile_LargeContent(t *testing.T) { + dir := t.TempDir() + testpath := filepath.Join(dir, "large_test") + + // Create a large content string + largeContent := strings.Repeat("HELM", 1024*1024) + reader := bytes.NewReader([]byte(largeContent)) + mode := os.FileMode(0644) + + err := AtomicWriteFile(testpath, reader, mode) + if err != nil { + t.Errorf("AtomicWriteFile error with large content: %s", err) + } + + got, err := os.ReadFile(testpath) + if err != nil { + t.Fatal(err) + } + + if largeContent != string(got) { + t.Fatalf("expected large content to match, got different length: %d vs %d", len(largeContent), len(got)) + } +} From 9112687a7e0f6fc85f5736dda48c263c30496795 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 12 Sep 2025 14:12:23 -0600 Subject: [PATCH 029/118] Update pkg/engine/engine.go Co-authored-by: George Jenkins Signed-off-by: Terry Howe --- pkg/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index a5532f73a..cce3c10c7 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -362,7 +362,7 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) { // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=191 traceableError, done := parseTemplateNoTemplateError(s, remainder) if done { - return traceableError, done + return traceableError, true } // Executing form: ": executing \"\" at <>: [ template:...]" From 91a65234ac52e1b693cf469a85faa78af79c2163 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 12 Sep 2025 14:26:53 -0600 Subject: [PATCH 030/118] Update pkg/engine/engine.go Co-authored-by: George Jenkins Signed-off-by: Terry Howe --- pkg/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index cce3c10c7..8272889d3 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -369,7 +369,7 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) { // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=141 traceableError, done = parseTemplateExecutingAtErrorType(remainder) if done { - return traceableError, done + return traceableError, true } // Simple form: ": " From b2870379c88f7bf9a51fe0aa3c1c6442a91cda68 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 12 Sep 2025 14:27:00 -0600 Subject: [PATCH 031/118] Update pkg/engine/engine.go Co-authored-by: George Jenkins Signed-off-by: Terry Howe --- pkg/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 8272889d3..9cbff272a 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -377,7 +377,7 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) { // Matches https://cs.opensource.google/go/go/+/refs/tags/go1.23.6:src/text/template/exec.go;l=138 traceableError, done = parseTemplateSimpleErrorString(remainder) if done { - return traceableError, done + return traceableError, true } return TraceableError{}, false From 1c67fbf108ec9849773897ae4ca1588dca5bdf1f Mon Sep 17 00:00:00 2001 From: reddaisyy Date: Mon, 15 Sep 2025 17:13:58 +0800 Subject: [PATCH 032/118] refactor: use strings.builder Signed-off-by: reddaisyy --- pkg/action/hooks_test.go | 9 +++++---- pkg/engine/engine_test.go | 6 +++--- pkg/strvals/literal_parser_test.go | 7 ++++--- pkg/strvals/parser_test.go | 7 ++++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 091155bc2..fb7d1b4ec 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -21,6 +21,7 @@ import ( "fmt" "io" "reflect" + "strings" "testing" "time" @@ -112,15 +113,15 @@ spec: } func convertHooksToCommaSeparated(hookDefinitions []release.HookOutputLogPolicy) string { - var commaSeparated string + var commaSeparated strings.Builder for i, policy := range hookDefinitions { if i+1 == len(hookDefinitions) { - commaSeparated += policy.String() + commaSeparated.WriteString(policy.String()) } else { - commaSeparated += policy.String() + "," + commaSeparated.WriteString(policy.String() + ",") } } - return commaSeparated + return commaSeparated.String() } func TestInstallRelease_HookOutputLogsOnFailure(t *testing.T) { diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 7ac892cec..364e96183 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -1024,15 +1024,15 @@ func TestRenderRecursionLimit(t *testing.T) { times := 4000 phrase := "All work and no play makes Jack a dull boy" printFunc := `{{define "overlook"}}{{printf "` + phrase + `\n"}}{{end}}` - var repeatedIncl string + var repeatedIncl strings.Builder for i := 0; i < times; i++ { - repeatedIncl += `{{include "overlook" . }}` + repeatedIncl.WriteString(`{{include "overlook" . }}`) } d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(repeatedIncl)}, + {Name: "templates/quote", Data: []byte(repeatedIncl.String())}, {Name: "templates/_function", Data: []byte(printFunc)}, }, } diff --git a/pkg/strvals/literal_parser_test.go b/pkg/strvals/literal_parser_test.go index 4e74423d6..6a76458f5 100644 --- a/pkg/strvals/literal_parser_test.go +++ b/pkg/strvals/literal_parser_test.go @@ -17,6 +17,7 @@ package strvals import ( "fmt" + "strings" "testing" "sigs.k8s.io/yaml" @@ -416,14 +417,14 @@ func TestParseLiteralInto(t *testing.T) { } func TestParseLiteralNestedLevels(t *testing.T) { - var keyMultipleNestedLevels string + var keyMultipleNestedLevels strings.Builder for i := 1; i <= MaxNestedNameLevel+2; i++ { tmpStr := fmt.Sprintf("name%d", i) if i <= MaxNestedNameLevel+1 { tmpStr = tmpStr + "." } - keyMultipleNestedLevels += tmpStr + keyMultipleNestedLevels.WriteString(tmpStr) } tests := []struct { @@ -439,7 +440,7 @@ func TestParseLiteralNestedLevels(t *testing.T) { "", }, { - str: keyMultipleNestedLevels + "=value", + str: keyMultipleNestedLevels.String() + "=value", err: true, errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), }, diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index a0c67b791..73403fc52 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -17,6 +17,7 @@ package strvals import ( "fmt" + "strings" "testing" "sigs.k8s.io/yaml" @@ -757,13 +758,13 @@ func TestToYAML(t *testing.T) { } func TestParseSetNestedLevels(t *testing.T) { - var keyMultipleNestedLevels string + var keyMultipleNestedLevels strings.Builder for i := 1; i <= MaxNestedNameLevel+2; i++ { tmpStr := fmt.Sprintf("name%d", i) if i <= MaxNestedNameLevel+1 { tmpStr = tmpStr + "." } - keyMultipleNestedLevels += tmpStr + keyMultipleNestedLevels.WriteString(tmpStr) } tests := []struct { str string @@ -778,7 +779,7 @@ func TestParseSetNestedLevels(t *testing.T) { "", }, { - str: keyMultipleNestedLevels + "=value", + str: keyMultipleNestedLevels.String() + "=value", err: true, errStr: fmt.Sprintf("value name nested level is greater than maximum supported nested level of %d", MaxNestedNameLevel), From 3e1dd9a5dcaa8e75edd7dbab6803d753068b852c Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 15 Sep 2025 12:28:09 -0600 Subject: [PATCH 033/118] chore: remove pkg/time which is no longer needed Signed-off-by: Terry Howe --- pkg/action/action.go | 2 +- pkg/action/action_test.go | 2 +- pkg/action/get_metadata_test.go | 19 ++- pkg/action/hooks.go | 7 +- pkg/action/install_test.go | 13 +- pkg/action/rollback.go | 3 +- pkg/action/uninstall.go | 3 +- pkg/action/upgrade_test.go | 3 +- pkg/cmd/flags_test.go | 4 +- pkg/cmd/helpers_test.go | 2 +- pkg/cmd/history.go | 13 +- pkg/cmd/list_test.go | 2 +- pkg/cmd/status_test.go | 7 +- .../output/status-with-resources.json | 2 +- pkg/cmd/testdata/output/status.json | 2 +- pkg/pusher/ocipusher.go | 3 +- pkg/registry/util.go | 3 +- pkg/registry/util_test.go | 11 +- pkg/release/v1/hook.go | 6 +- pkg/release/v1/info.go | 10 +- pkg/release/v1/mock.go | 2 +- pkg/release/v1/util/sorter_test.go | 3 +- pkg/time/ctime/ctime.go | 29 ---- pkg/time/ctime/ctime_linux.go | 30 ---- pkg/time/ctime/ctime_other.go | 27 ---- pkg/time/time.go | 92 ----------- pkg/time/time_test.go | 153 ------------------ 27 files changed, 55 insertions(+), 398 deletions(-) delete mode 100644 pkg/time/ctime/ctime.go delete mode 100644 pkg/time/ctime/ctime_linux.go delete mode 100644 pkg/time/ctime/ctime_other.go delete mode 100644 pkg/time/time.go delete mode 100644 pkg/time/time_test.go diff --git a/pkg/action/action.go b/pkg/action/action.go index bcf6ca8ef..1aa9f9d19 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -30,6 +30,7 @@ import ( "strings" "sync" "text/template" + "time" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -50,7 +51,6 @@ import ( releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" - "helm.sh/helm/v4/pkg/time" ) // Timestamper is a function capable of producing a timestamp.Timestamper. diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index b65e40024..78ca01089 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -24,6 +24,7 @@ import ( "log/slog" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,7 +39,6 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" - "helm.sh/helm/v4/pkg/time" ) var verbose = flag.Bool("test.log", false, "enable test logging (debug by default)") diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index ca612fed7..7962a2133 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -28,7 +28,6 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func TestNewGetMetadata(t *testing.T) { @@ -45,7 +44,7 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() rel := &release.Release{ Name: releaseName, @@ -86,7 +85,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() dependencies := []*chart.Dependency{ { @@ -138,7 +137,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() dependencies := []*chart.Dependency{ { @@ -194,7 +193,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() dependencies := []*chart.Dependency{ { @@ -268,7 +267,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() annotations := map[string]string{ "helm.sh/hook": "pre-install", @@ -313,13 +312,13 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) { client.Version = 2 releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() rel1 := &release.Release{ Name: releaseName, Info: &release.Info{ Status: release.StatusSuperseded, - LastDeployed: helmtime.Time{Time: deployedTime.Time.Add(-time.Hour)}, + LastDeployed: deployedTime.Add(-time.Hour), }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -384,7 +383,7 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { releaseName := "test-release-" + tc.name - deployedTime := helmtime.Now() + deployedTime := time.Now() rel := &release.Release{ Name: releaseName, @@ -440,7 +439,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) { client := NewGetMetadata(cfg) releaseName := "test-release" - deployedTime := helmtime.Now() + deployedTime := time.Now() rel := &release.Release{ Name: releaseName, diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 458a6342c..4808bc054 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -29,7 +29,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) // execHook executes all of the hooks for the given hook event. @@ -62,7 +61,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Record the time at which the hook was applied to the cluster h.LastRun = release.HookExecution{ - StartedAt: helmtime.Now(), + StartedAt: time.Now(), Phase: release.HookPhaseRunning, } cfg.recordRelease(rl) @@ -76,7 +75,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, if _, err := cfg.KubeClient.Create( resources, kube.ClientCreateOptionServerSideApply(serverSideApply, false)); err != nil { - h.LastRun.CompletedAt = helmtime.Now() + h.LastRun.CompletedAt = time.Now() h.LastRun.Phase = release.HookPhaseFailed return fmt.Errorf("warning: Hook %s %s failed: %w", hook, h.Path, err) } @@ -88,7 +87,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent, // Watch hook resources until they have completed err = waiter.WatchUntilReady(resources, timeout) // Note the time of success/failure - h.LastRun.CompletedAt = helmtime.Now() + h.LastRun.CompletedAt = time.Now() // Mark hook as succeeded or failed if err != nil { h.LastRun.Phase = release.HookPhaseFailed diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index b2b1508be..9031372d7 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -49,7 +49,6 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage/driver" - helmtime "helm.sh/helm/v4/pkg/time" ) type nameTemplateTestCase struct { @@ -856,32 +855,32 @@ func TestNameAndChartGenerateName(t *testing.T) { { "local filepath", "./chart", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "dot filepath", ".", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "empty filepath", "", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "packaged chart", "chart.tgz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "packaged chart with .tar.gz extension", "chart.tar.gz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, { "packaged chart with local extension", "./chart.tgz", - fmt.Sprintf("chart-%d", helmtime.Now().Unix()), + fmt.Sprintf("chart-%d", time.Now().Unix()), }, } diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index adaf22615..f56052988 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -26,7 +26,6 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) // Rollback is the action for rolling back to a given release. @@ -158,7 +157,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele Config: previousRelease.Config, Info: &release.Info{ FirstDeployed: currentRelease.Info.FirstDeployed, - LastDeployed: helmtime.Now(), + LastDeployed: time.Now(), Status: release.StatusPendingRollback, Notes: previousRelease.Info.Notes, // Because we lose the reference to previous version elsewhere, we set the diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 866be5d54..057c2118f 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -30,7 +30,6 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage/driver" - helmtime "helm.sh/helm/v4/pkg/time" ) // Uninstall is the action for uninstalling releases. @@ -110,7 +109,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) slog.Debug("uninstall: deleting release", "name", name) rel.Info.Status = release.StatusUninstalling - rel.Info.Deleted = helmtime.Now() + rel.Info.Deleted = time.Now() rel.Info.Description = "Deletion in progress (or silently failed)" res := &release.UninstallReleaseResponse{Release: rel} diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index d31804b87..0a436534f 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -34,7 +34,6 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func upgradeAction(t *testing.T) *Upgrade { @@ -260,7 +259,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { withValues(chartDefaultValues), withMetadataDependency(dependency), ) - now := helmtime.Now() + now := time.Now() existingValues := map[string]interface{}{ "subchart": map[string]interface{}{ "enabled": false, diff --git a/pkg/cmd/flags_test.go b/pkg/cmd/flags_test.go index dce748a6b..8d79716f0 100644 --- a/pkg/cmd/flags_test.go +++ b/pkg/cmd/flags_test.go @@ -19,19 +19,19 @@ package cmd import ( "fmt" "testing" + "time" "github.com/stretchr/testify/require" "helm.sh/helm/v4/pkg/action" chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func outputFlagCompletionTest(t *testing.T, cmdName string) { t.Helper() releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { - info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + info.LastDeployed = time.Unix(1452902400, 0).UTC() return []*release.Release{{ Name: "athos", Namespace: "default", diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 55e3a842f..96bf6434b 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -22,6 +22,7 @@ import ( "os" "strings" "testing" + "time" shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" @@ -34,7 +35,6 @@ import ( release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" - "helm.sh/helm/v4/pkg/time" ) func testTimestamper() time.Time { return time.Unix(242085845, 0).UTC() } diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index 9f029268c..f4dde95e4 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -31,7 +31,6 @@ import ( "helm.sh/helm/v4/pkg/cmd/require" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" - helmtime "helm.sh/helm/v4/pkg/time" ) var historyHelp = ` @@ -84,12 +83,12 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } type releaseInfo struct { - Revision int `json:"revision"` - Updated helmtime.Time `json:"updated"` - Status string `json:"status"` - Chart string `json:"chart"` - AppVersion string `json:"app_version"` - Description string `json:"description"` + Revision int `json:"revision"` + Updated time.Time `json:"updated,omitzero"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` } type releaseHistory []releaseInfo diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index 82b25a768..22a948fff 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -18,10 +18,10 @@ package cmd import ( "testing" + "time" chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" - "helm.sh/helm/v4/pkg/time" ) func TestListCmd(t *testing.T) { diff --git a/pkg/cmd/status_test.go b/pkg/cmd/status_test.go index cb4e23c59..8c251b76b 100644 --- a/pkg/cmd/status_test.go +++ b/pkg/cmd/status_test.go @@ -22,12 +22,11 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" release "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) func TestStatusCmd(t *testing.T) { releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release { - info.LastDeployed = helmtime.Unix(1452902400, 0).UTC() + info.LastDeployed = time.Unix(1452902400, 0).UTC() return []*release.Release{{ Name: "flummoxed-chickadee", Namespace: "default", @@ -130,8 +129,8 @@ func TestStatusCmd(t *testing.T) { runTestCmd(t, tests) } -func mustParseTime(t string) helmtime.Time { - res, _ := helmtime.Parse(time.RFC3339, t) +func mustParseTime(t string) time.Time { + res, _ := time.Parse(time.RFC3339, t) return res } diff --git a/pkg/cmd/testdata/output/status-with-resources.json b/pkg/cmd/testdata/output/status-with-resources.json index 275e0cfc6..af512bfd1 100644 --- a/pkg/cmd/testdata/output/status-with-resources.json +++ b/pkg/cmd/testdata/output/status-with-resources.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed"},"namespace":"default"} +{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed"},"namespace":"default"} diff --git a/pkg/cmd/testdata/output/status.json b/pkg/cmd/testdata/output/status.json index 4b499c935..4727dd100 100644 --- a/pkg/cmd/testdata/output/status.json +++ b/pkg/cmd/testdata/output/status.json @@ -1 +1 @@ -{"name":"flummoxed-chickadee","info":{"first_deployed":"","last_deployed":"2016-01-16T00:00:00Z","deleted":"","status":"deployed","notes":"release notes"},"namespace":"default"} +{"name":"flummoxed-chickadee","info":{"last_deployed":"2016-01-16T00:00:00Z","status":"deployed","notes":"release notes"},"namespace":"default"} diff --git a/pkg/pusher/ocipusher.go b/pkg/pusher/ocipusher.go index 699d27caf..25682969b 100644 --- a/pkg/pusher/ocipusher.go +++ b/pkg/pusher/ocipusher.go @@ -29,7 +29,6 @@ import ( "helm.sh/helm/v4/internal/tlsutil" "helm.sh/helm/v4/pkg/chart/v2/loader" "helm.sh/helm/v4/pkg/registry" - "helm.sh/helm/v4/pkg/time/ctime" ) // OCIPusher is the default OCI backend handler @@ -91,7 +90,7 @@ func (pusher *OCIPusher) push(chartRef, href string) error { meta.Metadata.Version) // The time the chart was "created" is semantically the time the chart archive file was last written(modified) - chartArchiveFileCreatedTime := ctime.Modified(stat) + chartArchiveFileCreatedTime := stat.ModTime() pushOpts = append(pushOpts, registry.PushOptCreationTime(chartArchiveFileCreatedTime.Format(time.RFC3339))) _, err = client.Push(chartBytes, ref, pushOpts...) diff --git a/pkg/registry/util.go b/pkg/registry/util.go index b31ab63fe..6071c66c3 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/util.go @@ -28,7 +28,6 @@ import ( "helm.sh/helm/v4/internal/tlsutil" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" - helmtime "helm.sh/helm/v4/pkg/time" "github.com/Masterminds/semver/v3" ocispec "github.com/opencontainers/image-spec/specs-go/v1" @@ -157,7 +156,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[ chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationURL, meta.Home) if len(creationTime) == 0 { - creationTime = helmtime.Now().UTC().Format(time.RFC3339) + creationTime = time.Now().UTC().Format(time.RFC3339) } chartOCIAnnotations = addToMap(chartOCIAnnotations, ocispec.AnnotationCreated, creationTime) diff --git a/pkg/registry/util_test.go b/pkg/registry/util_test.go index c8ce4e4a4..a67bc853a 100644 --- a/pkg/registry/util_test.go +++ b/pkg/registry/util_test.go @@ -24,12 +24,11 @@ import ( ocispec "github.com/opencontainers/image-spec/specs-go/v1" chart "helm.sh/helm/v4/pkg/chart/v2" - helmtime "helm.sh/helm/v4/pkg/time" ) func TestGenerateOCIChartAnnotations(t *testing.T) { - nowString := helmtime.Now().Format(time.RFC3339) + nowString := time.Now().Format(time.RFC3339) tests := []struct { name string @@ -160,7 +159,7 @@ func TestGenerateOCIChartAnnotations(t *testing.T) { func TestGenerateOCIAnnotations(t *testing.T) { - nowString := helmtime.Now().Format(time.RFC3339) + nowString := time.Now().Format(time.RFC3339) tests := []struct { name string @@ -234,7 +233,7 @@ func TestGenerateOCIAnnotations(t *testing.T) { func TestGenerateOCICreatedAnnotations(t *testing.T) { - nowTime := helmtime.Now() + nowTime := time.Now() nowTimeString := nowTime.Format(time.RFC3339) chart := &chart.Metadata{ @@ -250,7 +249,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { } // Verify value of created artifact in RFC3339 format - if _, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + if _, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) } @@ -262,7 +261,7 @@ func TestGenerateOCICreatedAnnotations(t *testing.T) { t.Errorf("%s annotation not created", ocispec.AnnotationCreated) } - if createdTimeAnnotation, err := helmtime.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { + if createdTimeAnnotation, err := time.Parse(time.RFC3339, result[ocispec.AnnotationCreated]); err != nil { t.Errorf("%s annotation with value '%s' not in RFC3339 format", ocispec.AnnotationCreated, result[ocispec.AnnotationCreated]) // Verify creation annotation after time test began diff --git a/pkg/release/v1/hook.go b/pkg/release/v1/hook.go index 1ef5c1eb8..b7d3c3992 100644 --- a/pkg/release/v1/hook.go +++ b/pkg/release/v1/hook.go @@ -17,7 +17,7 @@ limitations under the License. package v1 import ( - "helm.sh/helm/v4/pkg/time" + "time" ) // HookEvent specifies the hook event @@ -97,9 +97,9 @@ type Hook struct { // A HookExecution records the result for the last execution of a hook for a given release. type HookExecution struct { // StartedAt indicates the date/time this hook was started - StartedAt time.Time `json:"started_at,omitempty"` + StartedAt time.Time `json:"started_at,omitzero"` // CompletedAt indicates the date/time this hook was completed. - CompletedAt time.Time `json:"completed_at,omitempty"` + CompletedAt time.Time `json:"completed_at,omitzero"` // Phase indicates whether the hook completed successfully Phase HookPhase `json:"phase"` } diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index ff98ab63e..cef7e9960 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -16,19 +16,19 @@ limitations under the License. package v1 import ( - "k8s.io/apimachinery/pkg/runtime" + "time" - "helm.sh/helm/v4/pkg/time" + "k8s.io/apimachinery/pkg/runtime" ) // Info describes release information. type Info struct { // FirstDeployed is when the release was first deployed. - FirstDeployed time.Time `json:"first_deployed,omitempty"` + FirstDeployed time.Time `json:"first_deployed,omitzero"` // LastDeployed is when the release was last deployed. - LastDeployed time.Time `json:"last_deployed,omitempty"` + LastDeployed time.Time `json:"last_deployed,omitzero"` // Deleted tracks when this object was deleted. - Deleted time.Time `json:"deleted"` + Deleted time.Time `json:"deleted,omitzero"` // Description is human-friendly "log entry" about this release. Description string `json:"description,omitempty"` // Status is the current state of the release diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index c3a6594cc..818cd777e 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -19,10 +19,10 @@ package v1 import ( "fmt" "math/rand" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/time" ) // MockHookTemplate is the hook template used for all mock release objects. diff --git a/pkg/release/v1/util/sorter_test.go b/pkg/release/v1/util/sorter_test.go index 4628a5192..0889ddb94 100644 --- a/pkg/release/v1/util/sorter_test.go +++ b/pkg/release/v1/util/sorter_test.go @@ -21,7 +21,6 @@ import ( "time" rspb "helm.sh/helm/v4/pkg/release/v1" - helmtime "helm.sh/helm/v4/pkg/time" ) // note: this test data is shared with filter_test.go. @@ -34,7 +33,7 @@ var releases = []*rspb.Release{ } func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { - info := &rspb.Info{Status: status, LastDeployed: helmtime.Now().Add(dur)} + info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)} return &rspb.Release{ Name: name, Version: vers, diff --git a/pkg/time/ctime/ctime.go b/pkg/time/ctime/ctime.go deleted file mode 100644 index 63a41c0bf..000000000 --- a/pkg/time/ctime/ctime.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package ctime - -import ( - "os" - "time" -) - -func Created(fi os.FileInfo) time.Time { - return modified(fi) -} - -func Modified(fi os.FileInfo) time.Time { - return modified(fi) -} diff --git a/pkg/time/ctime/ctime_linux.go b/pkg/time/ctime/ctime_linux.go deleted file mode 100644 index d8a6ea1a1..000000000 --- a/pkg/time/ctime/ctime_linux.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build linux - -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package ctime - -import ( - "os" - "syscall" - "time" -) - -func modified(fi os.FileInfo) time.Time { - st := fi.Sys().(*syscall.Stat_t) - //nolint - return time.Unix(int64(st.Mtim.Sec), int64(st.Mtim.Nsec)) -} diff --git a/pkg/time/ctime/ctime_other.go b/pkg/time/ctime/ctime_other.go deleted file mode 100644 index 12afc6df2..000000000 --- a/pkg/time/ctime/ctime_other.go +++ /dev/null @@ -1,27 +0,0 @@ -//go:build !linux - -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ -package ctime - -import ( - "os" - "time" -) - -func modified(fi os.FileInfo) time.Time { - return fi.ModTime() -} diff --git a/pkg/time/time.go b/pkg/time/time.go deleted file mode 100644 index 16973b455..000000000 --- a/pkg/time/time.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -// Package time contains a wrapper for time.Time in the standard library and -// associated methods. This package mainly exists to work around an issue in Go -// where the serializer doesn't omit an empty value for time: -// https://github.com/golang/go/issues/11939. As such, this can be removed if a -// proposal is ever accepted for Go -package time - -import ( - "bytes" - "time" -) - -// emptyString contains an empty JSON string value to be used as output -var emptyString = `""` - -// Time is a convenience wrapper around stdlib time, but with different -// marshalling and unmarshalling for zero values -type Time struct { - time.Time -} - -// Now returns the current time. It is a convenience wrapper around time.Now() -func Now() Time { - return Time{time.Now()} -} - -func (t Time) MarshalJSON() ([]byte, error) { - if t.IsZero() { - return []byte(emptyString), nil - } - - return t.Time.MarshalJSON() -} - -func (t *Time) UnmarshalJSON(b []byte) error { - if bytes.Equal(b, []byte("null")) { - return nil - } - // If it is empty, we don't have to set anything since time.Time is not a - // pointer and will be set to the zero value - if bytes.Equal([]byte(emptyString), b) { - return nil - } - - return t.Time.UnmarshalJSON(b) -} - -func Parse(layout, value string) (Time, error) { - t, err := time.Parse(layout, value) - return Time{Time: t}, err -} - -func ParseInLocation(layout, value string, loc *time.Location) (Time, error) { - t, err := time.ParseInLocation(layout, value, loc) - return Time{Time: t}, err -} - -func Date(year int, month time.Month, day, hour, minute, second, nanoSecond int, loc *time.Location) Time { - return Time{Time: time.Date(year, month, day, hour, minute, second, nanoSecond, loc)} -} - -func Unix(sec int64, nsec int64) Time { return Time{Time: time.Unix(sec, nsec)} } - -func (t Time) Add(d time.Duration) Time { return Time{Time: t.Time.Add(d)} } -func (t Time) AddDate(years int, months int, days int) Time { - return Time{Time: t.Time.AddDate(years, months, days)} -} -func (t Time) After(u Time) bool { return t.Time.After(u.Time) } -func (t Time) Before(u Time) bool { return t.Time.Before(u.Time) } -func (t Time) Equal(u Time) bool { return t.Time.Equal(u.Time) } -func (t Time) In(loc *time.Location) Time { return Time{Time: t.Time.In(loc)} } -func (t Time) Local() Time { return Time{Time: t.Time.Local()} } -func (t Time) Round(d time.Duration) Time { return Time{Time: t.Time.Round(d)} } -func (t Time) Sub(u Time) time.Duration { return t.Time.Sub(u.Time) } -func (t Time) Truncate(d time.Duration) Time { return Time{Time: t.Time.Truncate(d)} } -func (t Time) UTC() Time { return Time{Time: t.Time.UTC()} } diff --git a/pkg/time/time_test.go b/pkg/time/time_test.go deleted file mode 100644 index 342ca4a10..000000000 --- a/pkg/time/time_test.go +++ /dev/null @@ -1,153 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package time - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - timeParseString = `"1977-09-02T22:04:05Z"` - timeString = "1977-09-02 22:04:05 +0000 UTC" -) - -func givenTime(t *testing.T) Time { - t.Helper() - result, err := Parse(time.RFC3339, "1977-09-02T22:04:05Z") - require.NoError(t, err) - return result -} - -func TestDate(t *testing.T) { - testingTime := givenTime(t) - got := Date(1977, 9, 2, 22, 04, 05, 0, time.UTC) - assert.Equal(t, timeString, got.String()) - assert.True(t, testingTime.Equal(got)) - assert.True(t, got.Equal(testingTime)) -} - -func TestNow(t *testing.T) { - testingTime := givenTime(t) - got := Now() - assert.True(t, testingTime.Before(got)) - assert.True(t, got.After(testingTime)) -} - -func TestTime_Add(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.Add(time.Hour) - assert.Equal(t, timeString, testingTime.String()) - assert.Equal(t, "1977-09-02 23:04:05 +0000 UTC", got.String()) -} - -func TestTime_AddDate(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.AddDate(1, 1, 1) - assert.Equal(t, "1978-10-03 22:04:05 +0000 UTC", got.String()) -} - -func TestTime_In(t *testing.T) { - testingTime := givenTime(t) - edt, err := time.LoadLocation("America/New_York") - assert.NoError(t, err) - got := testingTime.In(edt) - assert.Equal(t, "America/New_York", got.Location().String()) -} - -func TestTime_MarshalJSONNonZero(t *testing.T) { - testingTime := givenTime(t) - res, err := json.Marshal(testingTime) - assert.NoError(t, err) - assert.Equal(t, timeParseString, string(res)) -} - -func TestTime_MarshalJSONZeroValue(t *testing.T) { - res, err := json.Marshal(Time{}) - assert.NoError(t, err) - assert.Equal(t, `""`, string(res)) -} - -func TestTime_Round(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.Round(time.Hour) - assert.Equal(t, timeString, testingTime.String()) - assert.Equal(t, "1977-09-02 22:00:00 +0000 UTC", got.String()) -} - -func TestTime_Sub(t *testing.T) { - testingTime := givenTime(t) - before, err := Parse(time.RFC3339, "1977-09-01T22:04:05Z") - require.NoError(t, err) - got := testingTime.Sub(before) - assert.Equal(t, "24h0m0s", got.String()) -} - -func TestTime_Truncate(t *testing.T) { - testingTime := givenTime(t) - got := testingTime.Truncate(time.Hour) - assert.Equal(t, timeString, testingTime.String()) - assert.Equal(t, "1977-09-02 22:00:00 +0000 UTC", got.String()) -} - -func TestTime_UTC(t *testing.T) { - edtTime, err := Parse(time.RFC3339, "1977-09-03T05:04:05+07:00") - require.NoError(t, err) - got := edtTime.UTC() - assert.Equal(t, timeString, got.String()) -} - -func TestTime_UnmarshalJSONNonZeroValue(t *testing.T) { - testingTime := givenTime(t) - var myTime Time - err := json.Unmarshal([]byte(timeParseString), &myTime) - assert.NoError(t, err) - assert.True(t, testingTime.Equal(myTime)) -} - -func TestTime_UnmarshalJSONEmptyString(t *testing.T) { - var myTime Time - err := json.Unmarshal([]byte(emptyString), &myTime) - assert.NoError(t, err) - assert.True(t, myTime.IsZero()) -} - -func TestTime_UnmarshalJSONNullString(t *testing.T) { - var myTime Time - err := json.Unmarshal([]byte("null"), &myTime) - assert.NoError(t, err) - assert.True(t, myTime.IsZero()) -} - -func TestTime_UnmarshalJSONZeroValue(t *testing.T) { - // This test ensures that we can unmarshal any time value that was output - // with the current go default value of "0001-01-01T00:00:00Z" - var myTime Time - err := json.Unmarshal([]byte(`"0001-01-01T00:00:00Z"`), &myTime) - assert.NoError(t, err) - assert.True(t, myTime.IsZero()) -} - -func TestUnix(t *testing.T) { - got := Unix(242085845, 0) - assert.Equal(t, int64(242085845), got.Unix()) - assert.Equal(t, timeString, got.UTC().String()) -} From 838c36a0f6f89d07bae0e1e71d3f3abd6d1b0e67 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:28:08 +0000 Subject: [PATCH 034/118] Remove implicit support for helm lint current directory Signed-off-by: Terry Howe --- pkg/cmd/lint.go | 7 +++---- pkg/cmd/lint_test.go | 9 +++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/lint.go b/pkg/cmd/lint.go index 71540f1be..ccc53ddd0 100644 --- a/pkg/cmd/lint.go +++ b/pkg/cmd/lint.go @@ -30,6 +30,7 @@ import ( "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/v2/lint/support" "helm.sh/helm/v4/pkg/cli/values" + "helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/getter" ) @@ -51,11 +52,9 @@ func newLintCmd(out io.Writer) *cobra.Command { Use: "lint PATH", Short: "examine a chart for possible issues", Long: longLintHelp, + Args: require.MinimumNArgs(1), RunE: func(_ *cobra.Command, args []string) error { - paths := []string{"."} - if len(args) > 0 { - paths = args - } + paths := args if kubeVersion != "" { parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) diff --git a/pkg/cmd/lint_test.go b/pkg/cmd/lint_test.go index 401c84d74..270273116 100644 --- a/pkg/cmd/lint_test.go +++ b/pkg/cmd/lint_test.go @@ -91,6 +91,15 @@ func TestLintCmdWithKubeVersionFlag(t *testing.T) { runTestCmd(t, tests) } +func TestLintCmdRequiresArgs(t *testing.T) { + tests := []cmdTestCase{{ + name: "lint without arguments should fail", + cmd: "lint", + wantError: true, + }} + runTestCmd(t, tests) +} + func TestLintFileCompletion(t *testing.T) { checkFileCompletion(t, "lint", true) checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given From dab4fd2909b98c662d86d8ae4d90dbd1416230d5 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 15 Sep 2025 15:44:17 -0600 Subject: [PATCH 035/118] fix: hide notes in helm test command Signed-off-by: Terry Howe --- pkg/action/release_testing.go | 1 - pkg/cmd/release_testing.go | 3 +- pkg/cmd/release_testing_test.go | 50 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index 009f4d793..d00c8d8c0 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -45,7 +45,6 @@ type ReleaseTesting struct { // Used for fetching logs from test pods Namespace string Filters map[string][]string - HideNotes bool } // NewReleaseTesting creates a new ReleaseTesting object with the given configuration. diff --git a/pkg/cmd/release_testing.go b/pkg/cmd/release_testing.go index b660a16c5..e02604f63 100644 --- a/pkg/cmd/release_testing.go +++ b/pkg/cmd/release_testing.go @@ -77,7 +77,7 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command release: rel, debug: settings.Debug, showMetadata: false, - hideNotes: client.HideNotes, + hideNotes: true, noColor: settings.ShouldDisableColor(), }); err != nil { return err @@ -99,7 +99,6 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&outputLogs, "logs", false, "dump the logs from test pods (this runs after all tests are complete, but before any cleanup)") f.StringSliceVar(&filter, "filter", []string{}, "specify tests by attribute (currently \"name\") using attribute=value syntax or '!attribute=value' to exclude a test (can specify multiple or separate values with commas: name=test1,name=test2)") - f.BoolVar(&client.HideNotes, "hide-notes", false, "if set, do not show notes in test output. Does not affect presence in chart metadata") return cmd } diff --git a/pkg/cmd/release_testing_test.go b/pkg/cmd/release_testing_test.go index 43599ad0d..0c46755ac 100644 --- a/pkg/cmd/release_testing_test.go +++ b/pkg/cmd/release_testing_test.go @@ -17,7 +17,16 @@ limitations under the License. package cmd import ( + "bytes" + "io" + "strings" "testing" + + "helm.sh/helm/v4/pkg/action" + "helm.sh/helm/v4/pkg/chart/common" + chart "helm.sh/helm/v4/pkg/chart/v2" + kubefake "helm.sh/helm/v4/pkg/kube/fake" + release "helm.sh/helm/v4/pkg/release/v1" ) func TestReleaseTestingCompletion(t *testing.T) { @@ -28,3 +37,44 @@ func TestReleaseTestingFileCompletion(t *testing.T) { checkFileCompletion(t, "test", false) checkFileCompletion(t, "test myrelease", false) } + +func TestReleaseTestNotesHandling(t *testing.T) { + // Test that ensures notes behavior is correct for test command + // This is a simpler test that focuses on the core functionality + + rel := &release.Release{ + Name: "test-release", + Namespace: "default", + Info: &release.Info{ + Status: release.StatusDeployed, + Notes: "Some important notes that should be hidden by default", + }, + Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test", Version: "1.0.0"}}, + } + + // Set up storage + store := storageFixture() + store.Create(rel) + + // Set up action configuration properly + actionConfig := &action.Configuration{ + Releases: store, + KubeClient: &kubefake.FailingKubeClient{PrintingKubeClient: kubefake.PrintingKubeClient{Out: io.Discard}}, + Capabilities: common.DefaultCapabilities, + } + + // Test the newReleaseTestCmd function directly + var buf1 bytes.Buffer + + // Test 1: Default behavior (should hide notes) + cmd1 := newReleaseTestCmd(actionConfig, &buf1) + cmd1.SetArgs([]string{"test-release"}) + err1 := cmd1.Execute() + if err1 != nil { + t.Fatalf("Unexpected error for default test: %v", err1) + } + output1 := buf1.String() + if strings.Contains(output1, "NOTES:") { + t.Errorf("Expected notes to be hidden by default, but found NOTES section in output: %s", output1) + } +} From 469f4b457cfe0c55b421c25ab2d071963888e1fb Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 16 Sep 2025 15:26:39 -0600 Subject: [PATCH 036/118] chore: remove helm version client option Signed-off-by: Terry Howe --- pkg/cmd/testdata/output/version-client-shorthand.txt | 1 - pkg/cmd/testdata/output/version-client.txt | 1 - pkg/cmd/version.go | 4 +--- pkg/cmd/version_test.go | 8 -------- 4 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 pkg/cmd/testdata/output/version-client-shorthand.txt delete mode 100644 pkg/cmd/testdata/output/version-client.txt diff --git a/pkg/cmd/testdata/output/version-client-shorthand.txt b/pkg/cmd/testdata/output/version-client-shorthand.txt deleted file mode 100644 index 3b138ae77..000000000 --- a/pkg/cmd/testdata/output/version-client-shorthand.txt +++ /dev/null @@ -1 +0,0 @@ -version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/pkg/cmd/testdata/output/version-client.txt b/pkg/cmd/testdata/output/version-client.txt deleted file mode 100644 index 3b138ae77..000000000 --- a/pkg/cmd/testdata/output/version-client.txt +++ /dev/null @@ -1 +0,0 @@ -version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:""} diff --git a/pkg/cmd/version.go b/pkg/cmd/version.go index 0211716fe..80fb0d712 100644 --- a/pkg/cmd/version.go +++ b/pkg/cmd/version.go @@ -62,7 +62,7 @@ func newVersionCmd(out io.Writer) *cobra.Command { cmd := &cobra.Command{ Use: "version", - Short: "print the client version information", + Short: "print the helm version information", Long: versionDesc, Args: require.NoArgs, ValidArgsFunction: noMoreArgsCompFunc, @@ -73,8 +73,6 @@ func newVersionCmd(out io.Writer) *cobra.Command { f := cmd.Flags() f.BoolVar(&o.short, "short", false, "print the version number") f.StringVar(&o.template, "template", "", "template for version string format") - f.BoolP("client", "c", true, "display client version information") - f.MarkHidden("client") return cmd } diff --git a/pkg/cmd/version_test.go b/pkg/cmd/version_test.go index c06c72309..9551de767 100644 --- a/pkg/cmd/version_test.go +++ b/pkg/cmd/version_test.go @@ -32,14 +32,6 @@ func TestVersion(t *testing.T) { name: "template", cmd: "version --template='Version: {{.Version}}'", golden: "output/version-template.txt", - }, { - name: "client", - cmd: "version --client", - golden: "output/version-client.txt", - }, { - name: "client shorthand", - cmd: "version -c", - golden: "output/version-client-shorthand.txt", }} runTestCmd(t, tests) } From 020a8eccfd6474f171a1f7a5fca2fec88834a863 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 17 Sep 2025 08:50:12 -0600 Subject: [PATCH 037/118] fix: helm verify run signature Signed-off-by: Terry Howe --- pkg/action/verify.go | 18 ++++++------------ pkg/cmd/verify.go | 4 ++-- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/pkg/action/verify.go b/pkg/action/verify.go index ca2f4fa63..6e4562f61 100644 --- a/pkg/action/verify.go +++ b/pkg/action/verify.go @@ -28,7 +28,6 @@ import ( // It provides the implementation of 'helm verify'. type Verify struct { Keyring string - Out string } // NewVerify creates a new Verify object with the given configuration. @@ -37,23 +36,18 @@ func NewVerify() *Verify { } // Run executes 'helm verify'. -func (v *Verify) Run(chartfile string) error { +func (v *Verify) Run(chartfile string) (string, error) { var out strings.Builder p, err := downloader.VerifyChart(chartfile, chartfile+".prov", v.Keyring) if err != nil { - return err + return "", err } for name := range p.SignedBy.Identities { - fmt.Fprintf(&out, "Signed by: %v\n", name) + _, _ = fmt.Fprintf(&out, "Signed by: %v\n", name) } - fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint) - fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash) + _, _ = fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint) + _, _ = fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash) - // TODO(mattfarina): The output is set as a property rather than returned - // to maintain the Go API. In Helm v4 this function should return the out - // and the property on the struct can be removed. - v.Out = out.String() - - return nil + return out.String(), err } diff --git a/pkg/cmd/verify.go b/pkg/cmd/verify.go index 50f1ea914..3b7574386 100644 --- a/pkg/cmd/verify.go +++ b/pkg/cmd/verify.go @@ -53,12 +53,12 @@ func newVerifyCmd(out io.Writer) *cobra.Command { return noMoreArgsComp() }, RunE: func(_ *cobra.Command, args []string) error { - err := client.Run(args[0]) + result, err := client.Run(args[0]) if err != nil { return err } - fmt.Fprint(out, client.Out) + fmt.Fprint(out, result) return nil }, From 8dc7c57f504603fa6e8c3359f48cf76f40663821 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 17 Sep 2025 15:19:02 -0400 Subject: [PATCH 038/118] Update the action interfaces for chart apiversions This change is about handling the interfaces to public functions for different chart apiVersions. The internals are still focused on v2. This enables v3 to be layered in layer. Signed-off-by: Matt Farina --- internal/chart/v3/loader/archive.go | 166 +-------------- internal/chart/v3/loader/directory.go | 9 +- internal/chart/v3/loader/load.go | 15 +- internal/chart/v3/loader/load_test.go | 7 +- internal/chart/v3/util/expand.go | 4 +- pkg/action/install.go | 36 +++- pkg/action/install_test.go | 3 +- pkg/action/package.go | 35 +++- pkg/action/upgrade.go | 23 ++- pkg/chart/common.go | 24 +++ pkg/chart/dependency.go | 56 +++++ pkg/chart/interfaces.go | 8 + pkg/chart/loader/archive/archive.go | 195 ++++++++++++++++++ .../chart/loader/archive}/archive_test.go | 2 +- pkg/chart/loader/load.go | 163 +++++++++++++++ pkg/chart/v2/loader/archive.go | 166 +-------------- pkg/chart/v2/loader/archive_test.go | 92 --------- pkg/chart/v2/loader/directory.go | 9 +- pkg/chart/v2/loader/load.go | 15 +- pkg/chart/v2/loader/load_test.go | 11 +- pkg/chart/v2/util/expand.go | 4 +- pkg/cmd/install.go | 23 ++- pkg/cmd/upgrade.go | 12 +- 23 files changed, 590 insertions(+), 488 deletions(-) create mode 100644 pkg/chart/dependency.go create mode 100644 pkg/chart/loader/archive/archive.go rename {internal/chart/v3/loader => pkg/chart/loader/archive}/archive_test.go (99%) create mode 100644 pkg/chart/loader/load.go delete mode 100644 pkg/chart/v2/loader/archive_test.go diff --git a/internal/chart/v3/loader/archive.go b/internal/chart/v3/loader/archive.go index 311959d56..358c2ce4d 100644 --- a/internal/chart/v3/loader/archive.go +++ b/internal/chart/v3/loader/archive.go @@ -17,32 +17,16 @@ 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" + "helm.sh/helm/v4/pkg/chart/loader/archive" ) -// 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 @@ -65,7 +49,7 @@ func LoadFile(name string) (*chart.Chart, error) { } defer raw.Close() - err = ensureArchive(name, raw) + err = archive.EnsureArchive(name, raw) if err != nil { return nil, err } @@ -79,153 +63,9 @@ func LoadFile(name string) (*chart.Chart, error) { 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) + files, err := archive.LoadArchiveFiles(in) if err != nil { return nil, err } diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index 947051604..8cb7323dc 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -25,6 +25,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/sympath" + "helm.sh/helm/v4/pkg/chart/loader/archive" "helm.sh/helm/v4/pkg/ignore" ) @@ -61,7 +62,7 @@ func LoadDir(dir string) (*chart.Chart, error) { } rules.AddDefaults() - files := []*BufferedFile{} + files := []*archive.BufferedFile{} topdir += string(filepath.Separator) walk := func(name string, fi os.FileInfo, err error) error { @@ -99,8 +100,8 @@ func LoadDir(dir string) (*chart.Chart, error) { 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) + if fi.Size() > archive.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) } data, err := os.ReadFile(name) @@ -110,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) { data = bytes.TrimPrefix(data, utf8bom) - files = append(files, &BufferedFile{Name: n, Data: data}) + files = append(files, &archive.BufferedFile{Name: n, Data: data}) return nil } if err = sympath.Walk(topdir, walk); err != nil { diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 2959fc71d..b1b4bba8f 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -32,6 +32,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/loader/archive" ) // ChartLoader loads a chart. @@ -66,16 +67,10 @@ func Load(name string) (*chart.Chart, error) { 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) { +func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c := new(chart.Chart) - subcharts := make(map[string][]*BufferedFile) + subcharts := make(map[string][]*archive.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 @@ -125,7 +120,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { fname := strings.TrimPrefix(f.Name, "charts/") cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data}) default: c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } @@ -155,7 +150,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { 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)) + buff := make([]*archive.BufferedFile, 0, len(files)) for _, f := range files { parts := strings.SplitN(f.Name, "/", 2) if len(parts) < 2 { diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index 1d8ca836a..9f41429cc 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -32,6 +32,7 @@ import ( chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/loader/archive" ) func TestLoadDir(t *testing.T) { @@ -183,7 +184,7 @@ func TestLoadFile(t *testing.T) { } func TestLoadFiles(t *testing.T) { - goodFiles := []*BufferedFile{ + goodFiles := []*archive.BufferedFile{ { Name: "Chart.yaml", Data: []byte(`apiVersion: v3 @@ -248,7 +249,7 @@ icon: https://example.com/64x64.png t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) } - if _, err = LoadFiles([]*BufferedFile{}); err == nil { + if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil { t.Fatal("Expected err to be non-nil") } if err.Error() != "Chart.yaml file is missing" { @@ -259,7 +260,7 @@ icon: https://example.com/64x64.png // 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{ + goodFiles := []*archive.BufferedFile{ { Name: "requirements.yaml", Data: []byte("dependencies:"), diff --git a/internal/chart/v3/util/expand.go b/internal/chart/v3/util/expand.go index 6cbbeabf2..1a10fce3c 100644 --- a/internal/chart/v3/util/expand.go +++ b/internal/chart/v3/util/expand.go @@ -27,12 +27,12 @@ import ( "sigs.k8s.io/yaml" chart "helm.sh/helm/v4/internal/chart/v3" - "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart/loader/archive" ) // Expand uncompresses and extracts a chart into the specified directory. func Expand(dir string, r io.Reader) error { - files, err := loader.LoadArchiveFiles(r) + files, err := archive.LoadArchiveFiles(r) if err != nil { return err } diff --git a/pkg/action/install.go b/pkg/action/install.go index 5ae12904d..c6d4f723c 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -42,6 +42,7 @@ import ( "k8s.io/cli-runtime/pkg/resource" "sigs.k8s.io/yaml" + ci "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common/util" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -243,7 +244,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error { // // If DryRun is set to true, this will prepare the release, but not install it -func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { +func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (*release.Release, error) { ctx := context.Background() return i.RunWithContext(ctx, chrt, vals) } @@ -252,7 +253,17 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // // When the task is cancelled through ctx, the function returns and the install // proceeds in the background. -func (i *Install) RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { +func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (*release.Release, error) { + var chrt *chart.Chart + switch c := ch.(type) { + case *chart.Chart: + chrt = c + case chart.Chart: + chrt = &c + default: + return nil, errors.New("invalid chart apiVersion") + } + // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) if !i.ClientOnly { if err := i.cfg.KubeClient.IsReachable(); err != nil { @@ -761,17 +772,30 @@ func TemplateName(nameTemplate string) (string, error) { } // CheckDependencies checks the dependencies for a chart. -func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { +func CheckDependencies(ch ci.Charter, reqs []ci.Dependency) error { + ac, err := ci.NewAccessor(ch) + if err != nil { + return err + } + var missing []string OUTER: for _, r := range reqs { - for _, d := range ch.Dependencies() { - if d.Name() == r.Name { + rac, err := ci.NewDependencyAccessor(r) + if err != nil { + return err + } + for _, d := range ac.Dependencies() { + dac, err := ci.NewAccessor(d) + if err != nil { + return err + } + if dac.Name() == rac.Name() { continue OUTER } } - missing = append(missing, r.Name) + missing = append(missing, rac.Name()) } if len(missing) > 0 { diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index b2b1508be..e90857a3f 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -1004,7 +1004,8 @@ func TestInstallRun_UnreachableKubeClient(t *testing.T) { instAction := NewInstall(config) instAction.ClientOnly = false ctx, done := context.WithCancel(t.Context()) - res, err := instAction.RunWithContext(ctx, nil, nil) + chrt := buildChart() + res, err := instAction.RunWithContext(ctx, chrt, nil) done() assert.Nil(t, res) diff --git a/pkg/action/package.go b/pkg/action/package.go index 6e762b507..92a9a8cb6 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -28,7 +28,9 @@ import ( "golang.org/x/term" "sigs.k8s.io/yaml" - "helm.sh/helm/v4/pkg/chart/v2/loader" + ci "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/loader" + chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/provenance" ) @@ -69,7 +71,21 @@ func NewPackage() *Package { // Run executes 'helm package' against the given chart and returns the path to the packaged chart. func (p *Package) Run(path string, _ map[string]interface{}) (string, error) { - ch, err := loader.LoadDir(path) + chrt, err := loader.LoadDir(path) + if err != nil { + return "", err + } + var ch *chart.Chart + switch c := chrt.(type) { + case *chart.Chart: + ch = c + case chart.Chart: + ch = &c + default: + return "", errors.New("invalid chart apiVersion") + } + + ac, err := ci.NewAccessor(ch) if err != nil { return "", err } @@ -87,7 +103,7 @@ func (p *Package) Run(path string, _ map[string]interface{}) (string, error) { ch.Metadata.AppVersion = p.AppVersion } - if reqs := ch.Metadata.Dependencies; reqs != nil { + if reqs := ac.MetaDependencies(); reqs != nil { if err := CheckDependencies(ch, reqs); err != nil { return "", err } @@ -146,13 +162,22 @@ func (p *Package) Clearsign(filename string) error { } // Load the chart archive to extract metadata - chart, err := loader.LoadFile(filename) + chrt, err := loader.LoadFile(filename) if err != nil { return fmt.Errorf("failed to load chart for signing: %w", err) } + var ch *chart.Chart + switch c := chrt.(type) { + case *chart.Chart: + ch = c + case chart.Chart: + ch = &c + default: + return errors.New("invalid chart apiVersion") + } // Marshal chart metadata to YAML bytes - metadataBytes, err := yaml.Marshal(chart.Metadata) + metadataBytes, err := yaml.Marshal(ch.Metadata) if err != nil { return fmt.Errorf("failed to marshal chart metadata: %w", err) } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3688adf0e..3c84570b2 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -28,9 +28,10 @@ import ( "k8s.io/cli-runtime/pkg/resource" + "helm.sh/helm/v4/pkg/chart" "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/chart/common/util" - chart "helm.sh/helm/v4/pkg/chart/v2" + chartv2 "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/postrenderer" @@ -151,17 +152,27 @@ func (u *Upgrade) SetRegistryClient(client *registry.Client) { } // Run executes the upgrade on the given release. -func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { +func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]interface{}) (*release.Release, error) { ctx := context.Background() return u.RunWithContext(ctx, name, chart, vals) } // RunWithContext executes the upgrade on the given release with context. -func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { +func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Charter, vals map[string]interface{}) (*release.Release, error) { if err := u.cfg.KubeClient.IsReachable(); err != nil { return nil, err } + var chrt *chartv2.Chart + switch c := ch.(type) { + case *chartv2.Chart: + chrt = c + case chartv2.Chart: + chrt = &c + default: + return nil, errors.New("invalid chart apiVersion") + } + // Make sure wait is set if RollbackOnFailure. This makes it so // the user doesn't have to specify both if u.WaitStrategy == kube.HookOnlyStrategy && u.RollbackOnFailure { @@ -173,7 +184,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, chart *chart. } slog.Debug("preparing upgrade", "name", name) - currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chart, vals) + currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals) if err != nil { return nil, err } @@ -206,7 +217,7 @@ func (u *Upgrade) isDryRun() bool { } // prepareUpgrade builds an upgraded release for an upgrade operation. -func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) { +func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) { if chart == nil { return nil, nil, false, errMissingChart } @@ -578,7 +589,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e // // This is skipped if the u.ResetValues flag is set, in which case the // request values are not altered. -func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) { +func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) { if u.ResetValues { // If ResetValues is set, we completely ignore current.Config. slog.Debug("resetting values to the chart's original version") diff --git a/pkg/chart/common.go b/pkg/chart/common.go index 8b1dd58c3..8080f3dc8 100644 --- a/pkg/chart/common.go +++ b/pkg/chart/common.go @@ -93,6 +93,14 @@ func (r *v2Accessor) Dependencies() []Charter { return deps } +func (r *v2Accessor) MetaDependencies() []Dependency { + var deps = make([]Dependency, len(r.chrt.Metadata.Dependencies)) + for i, c := range r.chrt.Metadata.Dependencies { + deps[i] = c + } + return deps +} + func (r *v2Accessor) Values() map[string]interface{} { return r.chrt.Values } @@ -101,6 +109,10 @@ func (r *v2Accessor) Schema() []byte { return r.chrt.Schema } +func (r *v2Accessor) Deprecated() bool { + return r.chrt.Metadata.Deprecated +} + type v3Accessor struct { chrt *v3chart.Chart } @@ -150,6 +162,14 @@ func (r *v3Accessor) Dependencies() []Charter { return deps } +func (r *v3Accessor) MetaDependencies() []Dependency { + var deps = make([]Dependency, len(r.chrt.Dependencies())) + for i, c := range r.chrt.Metadata.Dependencies { + deps[i] = c + } + return deps +} + func (r *v3Accessor) Values() map[string]interface{} { return r.chrt.Values } @@ -158,6 +178,10 @@ func (r *v3Accessor) Schema() []byte { return r.chrt.Schema } +func (r *v3Accessor) Deprecated() bool { + return r.chrt.Metadata.Deprecated +} + func structToMap(obj interface{}) (map[string]interface{}, error) { objValue := reflect.ValueOf(obj) diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go new file mode 100644 index 000000000..9f7c90364 --- /dev/null +++ b/pkg/chart/dependency.go @@ -0,0 +1,56 @@ +/* +Copyright The Helm Authors. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package chart + +import ( + "errors" + + v3chart "helm.sh/helm/v4/internal/chart/v3" + v2chart "helm.sh/helm/v4/pkg/chart/v2" +) + +var NewDependencyAccessor func(dep Dependency) (DependencyAccessor, error) = NewDefaultDependencyAccessor //nolint:revive + +func NewDefaultDependencyAccessor(dep Dependency) (DependencyAccessor, error) { + switch v := dep.(type) { + case v2chart.Dependency: + return &v2DependencyAccessor{&v}, nil + case *v2chart.Dependency: + return &v2DependencyAccessor{v}, nil + case v3chart.Dependency: + return &v3DependencyAccessor{&v}, nil + case *v3chart.Dependency: + return &v3DependencyAccessor{v}, nil + default: + return nil, errors.New("unsupported chart dependency type") + } +} + +type v2DependencyAccessor struct { + dep *v2chart.Dependency +} + +func (r *v2DependencyAccessor) Name() string { + return r.dep.Name +} + +type v3DependencyAccessor struct { + dep *v3chart.Dependency +} + +func (r *v3DependencyAccessor) Name() string { + return r.dep.Name +} diff --git a/pkg/chart/interfaces.go b/pkg/chart/interfaces.go index e87dd2c08..f9c61c35c 100644 --- a/pkg/chart/interfaces.go +++ b/pkg/chart/interfaces.go @@ -21,6 +21,8 @@ import ( type Charter interface{} +type Dependency interface{} + type Accessor interface { Name() string IsRoot() bool @@ -30,6 +32,12 @@ type Accessor interface { ChartFullPath() string IsLibraryChart() bool Dependencies() []Charter + MetaDependencies() []Dependency Values() map[string]interface{} Schema() []byte + Deprecated() bool +} + +type DependencyAccessor interface { + Name() string } diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go new file mode 100644 index 000000000..4d4ca4391 --- /dev/null +++ b/pkg/chart/loader/archive/archive.go @@ -0,0 +1,195 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// archive provides utility functions for working with Helm chart archive files +package archive + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + "regexp" + "strings" +) + +// MaxDecompressedChartSize is the maximum size of a chart archive that will be +// decompressed. This is the decompressed size of all the files. +// The default value is 100 MiB. +var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 100 MiB + +// MaxDecompressedFileSize is the size of the largest file that Helm will attempt to load. +// The size of the file is the decompressed version of it when it is stored in an archive. +var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB + +var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) + +var utf8bom = []byte{0xEF, 0xBB, 0xBF} + +// BufferedFile represents an archive file buffered for later processing. +type BufferedFile struct { + Name string + Data []byte +} + +// LoadArchiveFiles reads in files out of an archive into memory. This function +// performs important path security checks and should always be used before +// expanding a tarball +func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { + unzipped, err := gzip.NewReader(in) + if err != nil { + return nil, err + } + defer unzipped.Close() + + files := []*BufferedFile{} + tr := tar.NewReader(unzipped) + remainingSize := MaxDecompressedChartSize + for { + b := bytes.NewBuffer(nil) + hd, err := tr.Next() + if 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 +} + +// ensureArchive's job is to return an informative error if the file does not appear to be a gzipped archive. +// +// Sometimes users will provide a values.yaml for an argument where a chart is expected. One common occurrence +// of this is invoking `helm template values.yaml mychart` which would otherwise produce a confusing error +// if we didn't check for this. +func EnsureArchive(name string, raw *os.File) error { + defer raw.Seek(0, 0) // reset read offset to allow archive loading to proceed. + + // Check the file format to give us a chance to provide the user with more actionable feedback. + buffer := make([]byte, 512) + _, err := raw.Read(buffer) + if err != nil && err != io.EOF { + return fmt.Errorf("file '%s' cannot be read: %s", name, err) + } + + // Helm may identify achieve of the application/x-gzip as application/vnd.ms-fontobject. + // Fix for: https://github.com/helm/helm/issues/12261 + if contentType := http.DetectContentType(buffer); contentType != "application/x-gzip" && !isGZipApplication(buffer) { + // TODO: Is there a way to reliably test if a file content is YAML? ghodss/yaml accepts a wide + // variety of content (Makefile, .zshrc) as valid YAML without errors. + + // Wrong content type. Let's check if it's yaml and give an extra hint? + if strings.HasSuffix(name, ".yml") || strings.HasSuffix(name, ".yaml") { + return fmt.Errorf("file '%s' seems to be a YAML file, but expected a gzipped archive", name) + } + return fmt.Errorf("file '%s' does not appear to be a gzipped archive; got '%s'", name, contentType) + } + return nil +} + +// isGZipApplication checks whether the archive is of the application/x-gzip type. +func isGZipApplication(data []byte) bool { + sig := []byte("\x1F\x8B\x08") + return bytes.HasPrefix(data, sig) +} diff --git a/internal/chart/v3/loader/archive_test.go b/pkg/chart/loader/archive/archive_test.go similarity index 99% rename from internal/chart/v3/loader/archive_test.go rename to pkg/chart/loader/archive/archive_test.go index d16c47563..2fe09e9b2 100644 --- a/internal/chart/v3/loader/archive_test.go +++ b/pkg/chart/loader/archive/archive_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package loader +package archive import ( "archive/tar" diff --git a/pkg/chart/loader/load.go b/pkg/chart/loader/load.go new file mode 100644 index 000000000..7a5ddbca9 --- /dev/null +++ b/pkg/chart/loader/load.go @@ -0,0 +1,163 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "compress/gzip" + "errors" + "fmt" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + c3 "helm.sh/helm/v4/internal/chart/v3" + c3load "helm.sh/helm/v4/internal/chart/v3/loader" + "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/loader/archive" + c2 "helm.sh/helm/v4/pkg/chart/v2" + c2load "helm.sh/helm/v4/pkg/chart/v2/loader" +) + +// ChartLoader loads a chart. +type ChartLoader interface { + Load() (chart.Charter, 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.Charter, error) { + l, err := Loader(name) + if err != nil { + return nil, err + } + return l.Load() +} + +// DirLoader loads a chart from a directory +type DirLoader string + +// Load loads the chart +func (l DirLoader) Load() (chart.Charter, error) { + return LoadDir(string(l)) +} + +func LoadDir(dir string) (chart.Charter, error) { + topdir, err := filepath.Abs(dir) + if err != nil { + return nil, err + } + + name := filepath.Join(topdir, "Chart.yaml") + data, err := os.ReadFile(name) + if err != nil { + return nil, fmt.Errorf("unable to detect chart at %s: %w", name, err) + } + + c := new(chartBase) + err = yaml.Unmarshal(data, c) + if err != nil { + return nil, fmt.Errorf("cannot load Chart.yaml: %w", err) + } + + switch c.APIVersion { + case c2.APIVersionV1, c2.APIVersionV2, "": + return c2load.Load(dir) + case c3.APIVersionV3: + return c3load.Load(dir) + default: + return nil, errors.New("unsupported chart version") + } + +} + +// FileLoader loads a chart from a file +type FileLoader string + +// Load loads a chart +func (l FileLoader) Load() (chart.Charter, error) { + return LoadFile(string(l)) +} + +func LoadFile(name string) (chart.Charter, error) { + if fi, err := os.Stat(name); err != nil { + return nil, err + } else if fi.IsDir() { + return nil, errors.New("cannot load a directory") + } + + raw, err := os.Open(name) + if err != nil { + return nil, err + } + defer raw.Close() + + err = archive.EnsureArchive(name, raw) + if err != nil { + return nil, err + } + + files, err := archive.LoadArchiveFiles(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 nil, errors.New("unable to load chart archive") + } + + for _, f := range files { + if f.Name == "Chart.yaml" { + c := new(chartBase) + if err := yaml.Unmarshal(f.Data, c); err != nil { + return c, fmt.Errorf("cannot load Chart.yaml: %w", err) + } + switch c.APIVersion { + case c2.APIVersionV1, c2.APIVersionV2, "": + return c2load.Load(name) + case c3.APIVersionV3: + return c3load.Load(name) + default: + return nil, errors.New("unsupported chart version") + } + } + } + + return nil, errors.New("unable to detect chart version, no Chart.yaml found") +} + +// chartBase is used to detect the API Version for the chart to run it through the +// loader for that type. +type chartBase struct { + APIVersion string `json:"apiVersion,omitempty"` +} diff --git a/pkg/chart/v2/loader/archive.go b/pkg/chart/v2/loader/archive.go index b9f370f56..f6ed0e84f 100644 --- a/pkg/chart/v2/loader/archive.go +++ b/pkg/chart/v2/loader/archive.go @@ -17,32 +17,16 @@ limitations under the License. package loader import ( - "archive/tar" - "bytes" "compress/gzip" "errors" "fmt" "io" - "net/http" "os" - "path" - "regexp" - "strings" + "helm.sh/helm/v4/pkg/chart/loader/archive" chart "helm.sh/helm/v4/pkg/chart/v2" ) -// 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 @@ -65,7 +49,7 @@ func LoadFile(name string) (*chart.Chart, error) { } defer raw.Close() - err = ensureArchive(name, raw) + err = archive.EnsureArchive(name, raw) if err != nil { return nil, err } @@ -79,153 +63,9 @@ func LoadFile(name string) (*chart.Chart, error) { 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) + files, err := archive.LoadArchiveFiles(in) if err != nil { return nil, err } diff --git a/pkg/chart/v2/loader/archive_test.go b/pkg/chart/v2/loader/archive_test.go deleted file mode 100644 index d16c47563..000000000 --- a/pkg/chart/v2/loader/archive_test.go +++ /dev/null @@ -1,92 +0,0 @@ -/* -Copyright The Helm Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package 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/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index 4f72925dc..c6f31560c 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -24,6 +24,7 @@ import ( "strings" "helm.sh/helm/v4/internal/sympath" + "helm.sh/helm/v4/pkg/chart/loader/archive" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/ignore" ) @@ -61,7 +62,7 @@ func LoadDir(dir string) (*chart.Chart, error) { } rules.AddDefaults() - files := []*BufferedFile{} + files := []*archive.BufferedFile{} topdir += string(filepath.Separator) walk := func(name string, fi os.FileInfo, err error) error { @@ -99,8 +100,8 @@ func LoadDir(dir string) (*chart.Chart, error) { 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) + if fi.Size() > archive.MaxDecompressedFileSize { + return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize) } data, err := os.ReadFile(name) @@ -110,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) { data = bytes.TrimPrefix(data, utf8bom) - files = append(files, &BufferedFile{Name: n, Data: data}) + files = append(files, &archive.BufferedFile{Name: n, Data: data}) return nil } if err = sympath.Walk(topdir, walk); err != nil { diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 0c025e183..028d59e82 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -32,6 +32,7 @@ import ( "sigs.k8s.io/yaml" "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/loader/archive" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -67,16 +68,10 @@ func Load(name string) (*chart.Chart, error) { 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) { +func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c := new(chart.Chart) - subcharts := make(map[string][]*BufferedFile) + subcharts := make(map[string][]*archive.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 @@ -157,7 +152,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { fname := strings.TrimPrefix(f.Name, "charts/") cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &BufferedFile{Name: fname, Data: f.Data}) + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data}) default: c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) } @@ -187,7 +182,7 @@ func LoadFiles(files []*BufferedFile) (*chart.Chart, error) { 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)) + buff := make([]*archive.BufferedFile, 0, len(files)) for _, f := range files { parts := strings.SplitN(f.Name, "/", 2) if len(parts) < 2 { diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index c4ae646f6..7eca5f402 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -31,6 +31,7 @@ import ( "time" "helm.sh/helm/v4/pkg/chart/common" + "helm.sh/helm/v4/pkg/chart/loader/archive" chart "helm.sh/helm/v4/pkg/chart/v2" ) @@ -211,12 +212,12 @@ func TestLoadFile(t *testing.T) { func TestLoadFiles_BadCases(t *testing.T) { for _, tt := range []struct { name string - bufferedFiles []*BufferedFile + bufferedFiles []*archive.BufferedFile expectError string }{ { name: "These files contain only requirements.lock", - bufferedFiles: []*BufferedFile{ + bufferedFiles: []*archive.BufferedFile{ { Name: "requirements.lock", Data: []byte(""), @@ -235,7 +236,7 @@ func TestLoadFiles_BadCases(t *testing.T) { } func TestLoadFiles(t *testing.T) { - goodFiles := []*BufferedFile{ + goodFiles := []*archive.BufferedFile{ { Name: "Chart.yaml", Data: []byte(`apiVersion: v1 @@ -300,7 +301,7 @@ icon: https://example.com/64x64.png t.Errorf("Expected number of templates == 2, got %d", len(c.Templates)) } - if _, err = LoadFiles([]*BufferedFile{}); err == nil { + if _, err = LoadFiles([]*archive.BufferedFile{}); err == nil { t.Fatal("Expected err to be non-nil") } if err.Error() != "Chart.yaml file is missing" { @@ -311,7 +312,7 @@ icon: https://example.com/64x64.png // 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{ + goodFiles := []*archive.BufferedFile{ { Name: "requirements.yaml", Data: []byte("dependencies:"), diff --git a/pkg/chart/v2/util/expand.go b/pkg/chart/v2/util/expand.go index 9d08571ed..077dfbf38 100644 --- a/pkg/chart/v2/util/expand.go +++ b/pkg/chart/v2/util/expand.go @@ -26,13 +26,13 @@ import ( securejoin "github.com/cyphar/filepath-securejoin" "sigs.k8s.io/yaml" + "helm.sh/helm/v4/pkg/chart/loader/archive" chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/chart/v2/loader" ) // Expand uncompresses and extracts a chart into the specified directory. func Expand(dir string, r io.Reader) error { - files, err := loader.LoadArchiveFiles(r) + files, err := archive.LoadArchiveFiles(r) if err != nil { return err } diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index c4e121c1f..4f30bd7df 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -33,8 +33,8 @@ import ( "github.com/spf13/pflag" "helm.sh/helm/v4/pkg/action" - chart "helm.sh/helm/v4/pkg/chart/v2" - "helm.sh/helm/v4/pkg/chart/v2/loader" + "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/loader" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" @@ -270,15 +270,20 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options return nil, err } - if err := checkIfInstallable(chartRequested); err != nil { + ac, err := chart.NewAccessor(chartRequested) + if err != nil { return nil, err } - if chartRequested.Metadata.Deprecated { + if err := checkIfInstallable(ac); err != nil { + return nil, err + } + + if ac.Deprecated() { slog.Warn("this chart is deprecated") } - if req := chartRequested.Metadata.Dependencies; req != nil { + if req := ac.MetaDependencies(); req != nil { // If CheckDependencies returns an error, we have unfulfilled dependencies. // As of Helm 2.4.0, this is treated as a stopping condition: // https://github.com/helm/helm/issues/2209 @@ -337,12 +342,14 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options // checkIfInstallable validates if a chart can be installed // // Application chart type is only installable -func checkIfInstallable(ch *chart.Chart) error { - switch ch.Metadata.Type { +func checkIfInstallable(ch chart.Accessor) error { + meta := ch.MetadataAsMap() + + switch meta["Type"] { case "", "application": return nil } - return fmt.Errorf("%s charts are not installable", ch.Metadata.Type) + return fmt.Errorf("%s charts are not installable", meta["Type"]) } // Provide dynamic auto-completion for the install and template commands diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index c8fbf8bd3..fcc4f9294 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -30,7 +30,8 @@ import ( "github.com/spf13/cobra" "helm.sh/helm/v4/pkg/action" - "helm.sh/helm/v4/pkg/chart/v2/loader" + ci "helm.sh/helm/v4/pkg/chart" + "helm.sh/helm/v4/pkg/chart/loader" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cli/values" "helm.sh/helm/v4/pkg/cmd/require" @@ -198,7 +199,12 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - if req := ch.Metadata.Dependencies; req != nil { + + ac, err := ci.NewAccessor(ch) + if err != nil { + return err + } + if req := ac.MetaDependencies(); req != nil { if err := action.CheckDependencies(ch, req); err != nil { err = fmt.Errorf("an error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies: %w", err) if client.DependencyUpdate { @@ -226,7 +232,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } } - if ch.Metadata.Deprecated { + if ac.Deprecated() { slog.Warn("this chart is deprecated") } From 6bc6231bcc52534b064a293346f9e3f96a635d84 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Sun, 21 Sep 2025 07:50:50 -0700 Subject: [PATCH 039/118] Remove unused 'Settings' from plugin schema Signed-off-by: George Jenkins --- internal/plugin/schema/cli.go | 5 +---- pkg/cmd/load_plugins.go | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/plugin/schema/cli.go b/internal/plugin/schema/cli.go index 702b27e45..2282580f5 100644 --- a/internal/plugin/schema/cli.go +++ b/internal/plugin/schema/cli.go @@ -15,13 +15,10 @@ package schema import ( "bytes" - - "helm.sh/helm/v4/pkg/cli" ) type InputMessageCLIV1 struct { - ExtraArgs []string `json:"extraArgs"` - Settings *cli.EnvSettings `json:"settings"` + ExtraArgs []string `json:"extraArgs"` } type OutputMessageCLIV1 struct { diff --git a/pkg/cmd/load_plugins.go b/pkg/cmd/load_plugins.go index c0593f384..534113bde 100644 --- a/pkg/cmd/load_plugins.go +++ b/pkg/cmd/load_plugins.go @@ -113,7 +113,6 @@ func loadCLIPlugins(baseCmd *cobra.Command, out io.Writer) { input := &plugin.Input{ Message: schema.InputMessageCLIV1{ ExtraArgs: extraArgs, - Settings: settings, }, Env: env, Stdin: os.Stdin, From f21b143befbca1a6d930ba4ac95c2e7d2c56d3c2 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Sun, 4 May 2025 12:41:40 -0700 Subject: [PATCH 040/118] refactor: Replace action 'DryRun' string with DryRunStrategy type + deprecations Signed-off-by: George Jenkins --- pkg/action/action.go | 25 ++++++ pkg/action/action_test.go | 12 +++ pkg/action/install.go | 42 ++++------ pkg/action/install_test.go | 60 +++++++------- pkg/action/rollback.go | 12 +-- pkg/action/upgrade.go | 30 ++----- pkg/action/upgrade_test.go | 4 +- pkg/cmd/helpers.go | 83 ++++++++++++++++++++ pkg/cmd/helpers_test.go | 156 +++++++++++++++++++++++++++++++++++++ pkg/cmd/install.go | 41 +++------- pkg/cmd/rollback.go | 10 ++- pkg/cmd/template.go | 26 ++++--- pkg/cmd/upgrade.go | 22 ++---- 13 files changed, 374 insertions(+), 149 deletions(-) create mode 100644 pkg/cmd/helpers.go diff --git a/pkg/action/action.go b/pkg/action/action.go index 1aa9f9d19..a3db047b4 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -70,6 +70,21 @@ var ( errPending = errors.New("another operation (install/upgrade/rollback) is in progress") ) +type DryRunStrategy string + +const ( + // DryRunNone indicates the client will make all mutating calls + DryRunNone DryRunStrategy = "none" + + // DryRunClient, or client-side dry-run, indicates the client will avoid + // making calls to the server + DryRunClient DryRunStrategy = "client" + + // DryRunServer, or server-side dry-run, indicates the client will send + // calls to the APIServer with the dry-run parameter to prevent persisting changes + DryRunServer DryRunStrategy = "server" +) + // Configuration injects the dependencies that all actions share. type Configuration struct { // RESTClientGetter is an interface that loads Kubernetes clients. @@ -528,3 +543,13 @@ func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod { } return release.ApplyMethodClientSideApply } + +// isDryRun returns true if the strategy is set to run as a DryRun +func isDryRun(strategy DryRunStrategy) bool { + return strategy == DryRunClient || strategy == DryRunServer +} + +// interactWithServer determine whether or not to interact with a remote Kubernetes server +func interactWithServer(strategy DryRunStrategy) bool { + return strategy == DryRunNone || strategy == DryRunServer +} diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 78ca01089..d993da100 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -951,3 +951,15 @@ func TestDetermineReleaseSSAApplyMethod(t *testing.T) { assert.Equal(t, release.ApplyMethodClientSideApply, determineReleaseSSApplyMethod(false)) assert.Equal(t, release.ApplyMethodServerSideApply, determineReleaseSSApplyMethod(true)) } + +func TestIsDryRun(t *testing.T) { + assert.False(t, isDryRun(DryRunNone)) + assert.True(t, isDryRun(DryRunClient)) + assert.True(t, isDryRun(DryRunServer)) +} + +func TestInteractWithServer(t *testing.T) { + assert.True(t, interactWithServer(DryRunNone)) + assert.False(t, interactWithServer(DryRunClient)) + assert.True(t, interactWithServer(DryRunServer)) +} diff --git a/pkg/action/install.go b/pkg/action/install.go index c6d4f723c..1124845af 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -75,7 +75,6 @@ type Install struct { ChartPathOptions - ClientOnly bool // ForceReplace will, if set to `true`, ignore certain warnings and perform the install anyway. // // This should be used with caution. @@ -87,8 +86,8 @@ type Install struct { // see: https://kubernetes.io/docs/reference/using-api/server-side-apply/ ServerSideApply bool CreateNamespace bool - DryRun bool - DryRunOption string + // DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster + DryRunStrategy DryRunStrategy // 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 @@ -159,6 +158,7 @@ func NewInstall(cfg *Configuration) *Install { in := &Install{ cfg: cfg, ServerSideApply: true, + DryRunStrategy: DryRunNone, } in.registryClient = cfg.RegistryClient @@ -264,8 +264,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, errors.New("invalid chart apiVersion") } - // Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`) - if !i.ClientOnly { + if interactWithServer(i.DryRunStrategy) { if err := i.cfg.KubeClient.IsReachable(); err != nil { slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err)) return nil, fmt.Errorf("cluster reachability check failed: %w", err) @@ -273,7 +272,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st } // HideSecret must be used with dry run. Otherwise, return an error. - if !i.isDryRun() && i.HideSecret { + if !isDryRun(i.DryRunStrategy) && i.HideSecret { slog.Error("hiding Kubernetes secrets requires a dry-run mode") return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode") } @@ -288,23 +287,18 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st return nil, fmt.Errorf("chart dependencies processing failed: %w", err) } - var interactWithRemote bool - if !i.isDryRun() || i.DryRunOption == "server" || i.DryRunOption == "none" || i.DryRunOption == "false" { - interactWithRemote = true - } - // Pre-install anything in the crd/ directory. We do this before Helm // contacts the upstream server and builds the capabilities object. - if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 { + if crds := chrt.CRDObjects(); interactWithServer(i.DryRunStrategy) && !i.SkipCRDs && len(crds) > 0 { // On dry run, bail here - if i.isDryRun() { + if isDryRun(i.DryRunStrategy) { slog.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") } else if err := i.installCRDs(crds); err != nil { return nil, err } } - if i.ClientOnly { + if !interactWithServer(i.DryRunStrategy) { // Add mock objects in here so it doesn't use Kube API server // NOTE(bacongobbler): used for `helm template` i.cfg.Capabilities = common.DefaultCapabilities.Copy() @@ -317,7 +311,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st mem := driver.NewMemory() mem.SetNamespace(i.Namespace) i.cfg.Releases = storage.Init(mem) - } else if !i.ClientOnly && len(i.APIVersions) > 0 { + } else if interactWithServer(i.DryRunStrategy) && len(i.APIVersions) > 0 { slog.Debug("API Version list given outside of client only mode, this list will be ignored") } @@ -333,7 +327,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st } // special case for helm template --is-upgrade - isUpgrade := i.IsUpgrade && i.isDryRun() + isUpgrade := i.IsUpgrade && isDryRun(i.DryRunStrategy) options := common.ReleaseOptions{ Name: i.ReleaseName, Namespace: i.Namespace, @@ -353,7 +347,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st rel := i.createRelease(chrt, vals, i.Labels) var manifestDoc *bytes.Buffer - rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithRemote, i.EnableDNS, i.HideSecret) + rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, interactWithServer(i.DryRunStrategy), i.EnableDNS, i.HideSecret) // Even for errors, attach this if available if manifestDoc != nil { rel.Manifest = manifestDoc.String() @@ -386,7 +380,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st // we'll end up in a state where we will delete those resources upon // deleting the release because the manifest will be pointing at that // resource - if !i.ClientOnly && !isUpgrade && len(resources) > 0 { + if interactWithServer(i.DryRunStrategy) && !isUpgrade && len(resources) > 0 { if i.TakeOwnership { toBeAdopted, err = requireAdoption(resources) } else { @@ -398,7 +392,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st } // Bail out here if it is a dry run - if i.isDryRun() { + if isDryRun(i.DryRunStrategy) { rel.Info.Description = "Dry run complete" return rel, nil } @@ -480,14 +474,6 @@ func (i *Install) getGoroutineCount() int32 { return i.goroutineCount.Load() } -// isDryRun returns true if Upgrade is set to run as a DryRun -func (i *Install) isDryRun() bool { - if i.DryRun || i.DryRunOption == "client" || i.DryRunOption == "server" || i.DryRunOption == "true" { - return true - } - return false -} - func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) { var err error // pre-install hooks @@ -590,7 +576,7 @@ func (i *Install) availableName() error { return fmt.Errorf("release name %q: %w", start, err) } // On dry run, bail here - if i.isDryRun() { + if isDryRun(i.DryRunStrategy) { return nil } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index aae36152d..8d1fc149d 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -249,16 +249,6 @@ func TestInstallReleaseWithValues(t *testing.T) { is.Equal(expectedUserValues, rel.Config) } -func TestInstallReleaseClientOnly(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.ClientOnly = true - instAction.Run(buildChart(), nil) // disregard output - - is.Equal(instAction.cfg.Capabilities, common.DefaultCapabilities) - is.Equal(instAction.cfg.KubeClient, &kubefake.PrintingKubeClient{Out: io.Discard}) -} - func TestInstallRelease_NoName(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "" @@ -355,27 +345,30 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { is.Equal(rel.Info.Description, "Install complete") } -func TestInstallRelease_DryRun(t *testing.T) { - is := assert.New(t) - instAction := installAction(t) - instAction.DryRun = true - vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withSampleTemplates()), vals) - if err != nil { - t.Fatalf("Failed install: %s", err) - } +func TestInstallRelease_DryRunClient(t *testing.T) { + for _, dryRunStrategy := range []DryRunStrategy{DryRunClient, DryRunServer} { + is := assert.New(t) + instAction := installAction(t) + instAction.DryRunStrategy = dryRunStrategy - is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world") - is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") - is.Contains(res.Manifest, "hello: Earth") - is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}") - is.NotContains(res.Manifest, "empty") + vals := map[string]interface{}{} + res, err := instAction.Run(buildChart(withSampleTemplates()), vals) + if err != nil { + t.Fatalf("Failed install: %s", err) + } - _, err = instAction.cfg.Releases.Get(res.Name, res.Version) - is.Error(err) - is.Len(res.Hooks, 1) - is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run") - is.Equal(res.Info.Description, "Dry run complete") + is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world") + is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") + is.Contains(res.Manifest, "hello: Earth") + is.NotContains(res.Manifest, "hello: {{ template \"_planet\" . }}") + is.NotContains(res.Manifest, "empty") + + _, err = instAction.cfg.Releases.Get(res.Name, res.Version) + is.Error(err) + is.Len(res.Hooks, 1) + is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "expect hook to not be marked as run") + is.Equal(res.Info.Description, "Dry run complete") + } } func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { @@ -383,7 +376,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { instAction := installAction(t) // First perform a normal dry-run with the secret and confirm its presence. - instAction.DryRun = true + instAction.DryRunStrategy = DryRunClient vals := map[string]interface{}{} res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) if err != nil { @@ -410,7 +403,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { is.Equal(res2.Info.Description, "Dry run complete") // Ensure there is an error when HideSecret True but not in a dry-run mode - instAction.DryRun = false + instAction.DryRunStrategy = DryRunNone vals = map[string]interface{}{} _, err = instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) if err == nil { @@ -422,7 +415,7 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { func TestInstallRelease_DryRun_Lookup(t *testing.T) { is := assert.New(t) instAction := installAction(t) - instAction.DryRun = true + instAction.DryRunStrategy = DryRunNone vals := map[string]interface{}{} mockChart := buildChart(withSampleTemplates()) @@ -442,7 +435,7 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) { func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { is := assert.New(t) instAction := installAction(t) - instAction.DryRun = true + instAction.DryRunStrategy = DryRunNone vals := map[string]interface{}{} _, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals) expectedErr := `hello/templates/incorrect:1:10 @@ -1001,7 +994,6 @@ func TestInstallRun_UnreachableKubeClient(t *testing.T) { config.KubeClient = &failingKubeClient instAction := NewInstall(config) - instAction.ClientOnly = false ctx, done := context.WithCancel(t.Context()) chrt := buildChart() res, err := instAction.RunWithContext(ctx, chrt, nil) diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index f56052988..ade3ab233 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -39,7 +39,8 @@ type Rollback struct { WaitStrategy kube.WaitStrategy WaitForJobs bool DisableHooks bool - DryRun bool + // DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster + DryRunStrategy DryRunStrategy // ForceReplace will, if set to `true`, ignore certain warnings and perform the rollback anyway. // // This should be used with caution. @@ -59,7 +60,8 @@ type Rollback struct { // NewRollback creates a new Rollback object with the given configuration. func NewRollback(cfg *Configuration) *Rollback { return &Rollback{ - cfg: cfg, + cfg: cfg, + DryRunStrategy: DryRunNone, } } @@ -77,7 +79,7 @@ func (r *Rollback) Run(name string) error { return err } - if !r.DryRun { + if !isDryRun(r.DryRunStrategy) { slog.Debug("creating rolled back release", "name", name) if err := r.cfg.Releases.Create(targetRelease); err != nil { return err @@ -89,7 +91,7 @@ func (r *Rollback) Run(name string) error { return err } - if !r.DryRun { + if !isDryRun(r.DryRunStrategy) { slog.Debug("updating status for rolled back release", "name", name) if err := r.cfg.Releases.Update(targetRelease); err != nil { return err @@ -175,7 +177,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele } func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release, serverSideApply bool) (*release.Release, error) { - if r.DryRun { + if isDryRun(r.DryRunStrategy) { slog.Debug("dry run", "name", targetRelease.Name) return targetRelease, nil } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3c84570b2..8dfc8b206 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -73,10 +73,8 @@ type Upgrade struct { WaitForJobs bool // DisableHooks disables hook processing if set to true. DisableHooks bool - // DryRun controls whether the operation is prepared, but not executed. - DryRun bool - // DryRunOption controls whether the operation is prepared, but not executed with options on whether or not to interact with the remote cluster. - DryRunOption string + // DryRunStrategy can be set to prepare, but not execute the operation and whether or not to interact with the remote cluster + DryRunStrategy DryRunStrategy // 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 @@ -140,6 +138,7 @@ func NewUpgrade(cfg *Configuration) *Upgrade { up := &Upgrade{ cfg: cfg, ServerSideApply: "auto", + DryRunStrategy: DryRunNone, } up.registryClient = cfg.RegistryClient @@ -198,7 +197,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char } // Do not update for dry runs - if !u.isDryRun() { + if !isDryRun(u.DryRunStrategy) { slog.Debug("updating status for upgraded release", "name", name) if err := u.cfg.Releases.Update(upgradedRelease); err != nil { return res, err @@ -208,14 +207,6 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char return res, nil } -// isDryRun returns true if Upgrade is set to run as a DryRun -func (u *Upgrade) isDryRun() bool { - if u.DryRun || u.DryRunOption == "client" || u.DryRunOption == "server" || u.DryRunOption == "true" { - return true - } - return false -} - // prepareUpgrade builds an upgraded release for an upgrade operation. func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[string]interface{}) (*release.Release, *release.Release, bool, error) { if chart == nil { @@ -223,7 +214,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str } // HideSecret must be used with dry run. Otherwise, return an error. - if !u.isDryRun() && u.HideSecret { + if !isDryRun(u.DryRunStrategy) && u.HideSecret { return nil, nil, false, errors.New("hiding Kubernetes secrets requires a dry-run mode") } @@ -289,13 +280,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } - // Determine whether or not to interact with remote - var interactWithRemote bool - if !u.isDryRun() || u.DryRunOption == "server" || u.DryRunOption == "none" || u.DryRunOption == "false" { - interactWithRemote = true - } - - hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithRemote, u.EnableDNS, u.HideSecret) + hooks, manifestDoc, notesTxt, err := u.cfg.renderResources(chart, valuesToRender, "", "", u.SubNotes, false, false, u.PostRenderer, interactWithServer(u.DryRunStrategy), u.EnableDNS, u.HideSecret) if err != nil { return nil, nil, false, err } @@ -391,8 +376,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR return nil }) - // Run if it is a dry run - if u.isDryRun() { + if isDryRun(u.DryRunStrategy) { slog.Debug("dry run for release", "name", upgradedRelease.Name) if len(u.Description) > 0 { upgradedRelease.Info.Description = u.Description diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 0a436534f..34f1a2fb3 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -545,7 +545,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) { rel.Info.Status = release.StatusDeployed req.NoError(upAction.cfg.Releases.Create(rel)) - upAction.DryRun = true + upAction.DryRunStrategy = DryRunClient vals := map[string]interface{}{} ctx, done := context.WithCancel(t.Context()) @@ -577,7 +577,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) { is.Equal(1, lastRelease.Version) // Ensure in a dry run mode when using HideSecret - upAction.DryRun = false + upAction.DryRunStrategy = DryRunNone vals = map[string]interface{}{} ctx, done = context.WithCancel(t.Context()) diff --git a/pkg/cmd/helpers.go b/pkg/cmd/helpers.go new file mode 100644 index 000000000..e555dd18b --- /dev/null +++ b/pkg/cmd/helpers.go @@ -0,0 +1,83 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cmd + +import ( + "fmt" + "log/slog" + "strconv" + + "github.com/spf13/cobra" + + "helm.sh/helm/v4/pkg/action" +) + +func addDryRunFlag(cmd *cobra.Command) { + // --dry-run options with expected outcome: + // - Not set means no dry run and server is contacted. + // - Set with no value, a value of client, or a value of true and the server is not contacted + // - Set with a value of false, none, or false and the server is contacted + // The true/false part is meant to reflect some legacy behavior while none is equal to "". + f := cmd.Flags() + f.String( + "dry-run", + "none", + `simulates the operation without persisting changes. Must be one of: "none" (default), "client", or "server". '--dry-run=none' executes the operation normally and persists changes (no simulation). '--dry-run=client' simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates the operation on the server, requiring cluster connectivity.`) + f.Lookup("dry-run").NoOptDefVal = "unset" +} + +// Determine the `action.DryRunStrategy` given -dry-run=` flag (or absence of) +// Legacy usage of the flag: boolean values, and `--dry-run` (without value) are supported, and log warnings emitted +func cmdGetDryRunFlagStrategy(cmd *cobra.Command, isTemplate bool) (action.DryRunStrategy, error) { + + f := cmd.Flag("dry-run") + v := f.Value.String() + + switch v { + case f.NoOptDefVal: + slog.Warn(`--dry-run is deprecated and should be replaced with '--dry-run=client'`) + return action.DryRunClient, nil + case string(action.DryRunClient): + return action.DryRunClient, nil + case string(action.DryRunServer): + return action.DryRunServer, nil + case string(action.DryRunNone): + if isTemplate { + // Special case hack for `helm template`, which is always a dry run + return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v) + } + return action.DryRunNone, nil + } + + b, err := strconv.ParseBool(v) + if err != nil { + return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "none", "server", or "client"`, v) + } + + if isTemplate && !b { + // Special case for `helm template`, which is always a dry run + return action.DryRunNone, fmt.Errorf(`invalid dry-run value (%q). Must be "server" or "client"`, v) + } + + result := action.DryRunNone + if b { + result = action.DryRunClient + } + slog.Warn(fmt.Sprintf(`boolean '--dry-run=%v' flag is deprecated and must be replaced with '--dry-run=%s'`, v, result)) + + return result, nil +} diff --git a/pkg/cmd/helpers_test.go b/pkg/cmd/helpers_test.go index 96bf6434b..08065499e 100644 --- a/pkg/cmd/helpers_test.go +++ b/pkg/cmd/helpers_test.go @@ -18,7 +18,9 @@ package cmd import ( "bytes" + "encoding/json" "io" + "log/slog" "os" "strings" "testing" @@ -26,6 +28,8 @@ import ( shellwords "github.com/mattn/go-shellwords" "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "helm.sh/helm/v4/internal/test" "helm.sh/helm/v4/pkg/action" @@ -151,3 +155,155 @@ func resetEnv() func() { settings = cli.New() } } + +func TestCmdGetDryRunFlagStrategy(t *testing.T) { + + type testCaseExpectedLog struct { + Level string + Msg string + } + testCases := map[string]struct { + DryRunFlagArg string + IsTemplate bool + ExpectedStrategy action.DryRunStrategy + ExpectedError bool + ExpectedLog *testCaseExpectedLog + }{ + "unset_value": { + DryRunFlagArg: "--dry-run", + ExpectedStrategy: action.DryRunClient, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`, + }, + }, + "unset_special": { + DryRunFlagArg: "--dry-run=unset", // Special value that matches cmd.Flags("dry-run").NoOptDefVal + ExpectedStrategy: action.DryRunClient, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`, + }, + }, + "none": { + DryRunFlagArg: "--dry-run=none", + ExpectedStrategy: action.DryRunNone, + }, + "client": { + DryRunFlagArg: "--dry-run=client", + ExpectedStrategy: action.DryRunClient, + }, + "server": { + DryRunFlagArg: "--dry-run=server", + ExpectedStrategy: action.DryRunServer, + }, + "bool_false": { + DryRunFlagArg: "--dry-run=false", + ExpectedStrategy: action.DryRunNone, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `boolean '--dry-run=false' flag is deprecated and must be replaced with '--dry-run=none'`, + }, + }, + "bool_true": { + DryRunFlagArg: "--dry-run=true", + ExpectedStrategy: action.DryRunClient, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `boolean '--dry-run=true' flag is deprecated and must be replaced with '--dry-run=client'`, + }, + }, + "bool_0": { + DryRunFlagArg: "--dry-run=0", + ExpectedStrategy: action.DryRunNone, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `boolean '--dry-run=0' flag is deprecated and must be replaced with '--dry-run=none'`, + }, + }, + "bool_1": { + DryRunFlagArg: "--dry-run=1", + ExpectedStrategy: action.DryRunClient, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `boolean '--dry-run=1' flag is deprecated and must be replaced with '--dry-run=client'`, + }, + }, + "invalid": { + DryRunFlagArg: "--dry-run=invalid", + ExpectedError: true, + }, + "template_unset_value": { + DryRunFlagArg: "--dry-run", + IsTemplate: true, + ExpectedStrategy: action.DryRunClient, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `--dry-run is deprecated and should be replaced with '--dry-run=client'`, + }, + }, + "template_bool_false": { + DryRunFlagArg: "--dry-run=false", + IsTemplate: true, + ExpectedError: true, + }, + "template_bool_template_true": { + DryRunFlagArg: "--dry-run=true", + IsTemplate: true, + ExpectedStrategy: action.DryRunClient, + ExpectedLog: &testCaseExpectedLog{ + Level: "WARN", + Msg: `boolean '--dry-run=true' flag is deprecated and must be replaced with '--dry-run=client'`, + }, + }, + "template_none": { + DryRunFlagArg: "--dry-run=none", + IsTemplate: true, + ExpectedError: true, + }, + "template_client": { + DryRunFlagArg: "--dry-run=client", + IsTemplate: true, + ExpectedStrategy: action.DryRunClient, + }, + "template_server": { + DryRunFlagArg: "--dry-run=server", + IsTemplate: true, + ExpectedStrategy: action.DryRunServer, + }, + } + + for name, tc := range testCases { + + logBuf := new(bytes.Buffer) + logger := slog.New(slog.NewJSONHandler(logBuf, nil)) + slog.SetDefault(logger) + + cmd := &cobra.Command{ + Use: "helm", + } + addDryRunFlag(cmd) + cmd.Flags().Parse([]string{"helm", tc.DryRunFlagArg}) + + t.Run(name, func(t *testing.T) { + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, tc.IsTemplate) + if tc.ExpectedError { + assert.Error(t, err) + } else { + assert.Nil(t, err) + assert.Equal(t, tc.ExpectedStrategy, dryRunStrategy) + } + + if tc.ExpectedLog != nil { + logResult := map[string]string{} + err = json.Unmarshal(logBuf.Bytes(), &logResult) + require.Nil(t, err) + + assert.Equal(t, tc.ExpectedLog.Level, logResult["level"]) + assert.Equal(t, tc.ExpectedLog.Msg, logResult["msg"]) + } else { + assert.Equal(t, 0, logBuf.Len()) + } + }) + } +} diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 4f30bd7df..28bfa0b51 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -18,14 +18,12 @@ package cmd import ( "context" - "errors" "fmt" "io" "log" "log/slog" "os" "os/signal" - "slices" "syscall" "time" @@ -144,7 +142,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return compInstall(args, toComplete, client) }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, client.InsecureSkipTLSverify, client.PlainHTTP, client.Username, client.Password) if err != nil { @@ -152,12 +150,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetRegistryClient(registryClient) - // This is for the case where "" is specifically passed in as a - // value. When there is no value passed in NoOptDefVal will be used - // and it is set to client. See addInstallFlags. - if client.DryRunOption == "" { - client.DryRunOption = "none" + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) + if err != nil { + return err } + client.DryRunStrategy = dryRunStrategy + rel, err := runInstall(args, client, valueOpts, out) if err != nil { return fmt.Errorf("INSTALLATION FAILED: %w", err) @@ -173,11 +171,12 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }, } - addInstallFlags(cmd, cmd.Flags(), client, valueOpts) + f := cmd.Flags() + addInstallFlags(cmd, f, client, valueOpts) // hide-secret is not available in all places the install flags are used so // it is added separately - f := cmd.Flags() f.BoolVar(&client.HideSecret, "hide-secret", false, "hide Kubernetes Secrets when also using the --dry-run flag") + addDryRunFlag(cmd) bindOutputFlag(cmd, &outfmt) bindPostRenderFlag(cmd, &client.PostRenderer, settings) @@ -186,13 +185,6 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present") - // --dry-run options with expected outcome: - // - Not set means no dry run and server is contacted. - // - Set with no value, a value of client, or a value of true and the server is not contacted - // - Set with a value of false, none, or false and the server is contacted - // The true/false part is meant to reflect some legacy behavior while none is equal to "". - f.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.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.MarkDeprecated("force", "use --force-replace instead") @@ -316,11 +308,6 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options client.Namespace = settings.Namespace() - // Validate DryRunOption member is one of the allowed values - if err := validateDryRunOptionFlag(client.DryRunOption); err != nil { - return nil, err - } - // Create context and prepare the handle of SIGTERM ctx := context.Background() ctx, cancel := context.WithCancel(ctx) @@ -363,13 +350,3 @@ func compInstall(args []string, toComplete string, client *action.Install) ([]st } return nil, cobra.ShellCompDirectiveNoFileComp } - -func validateDryRunOptionFlag(dryRunOptionFlagValue string) error { - // Validate dry-run flag value with a set of allowed value - allowedDryRunValues := []string{"false", "true", "none", "client", "server"} - isAllowed := slices.Contains(allowedDryRunValues, dryRunOptionFlagValue) - if !isAllowed { - return errors.New("invalid dry-run flag. Flag must one of the following: false, true, none, client, server") - } - return nil -} diff --git a/pkg/cmd/rollback.go b/pkg/cmd/rollback.go index ff60aaedf..00a2725bc 100644 --- a/pkg/cmd/rollback.go +++ b/pkg/cmd/rollback.go @@ -57,7 +57,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return noMoreArgsComp() }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { if len(args) > 1 { ver, err := strconv.Atoi(args[1]) if err != nil { @@ -66,6 +66,12 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.Version = ver } + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) + if err != nil { + return err + } + client.DryRunStrategy = dryRunStrategy + if err := client.Run(args[0]); err != nil { return err } @@ -76,7 +82,6 @@ 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.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.MarkDeprecated("force", "use --force-replace instead") @@ -87,6 +92,7 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { 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") f.BoolVar(&client.CleanupOnFail, "cleanup-on-fail", false, "allow deletion of new resources created in this rollback when rollback fails") f.IntVar(&client.MaxHistory, "history-max", settings.MaxHistory, "limit the maximum number of revisions saved per release. Use 0 for no limit") + addDryRunFlag(cmd) AddWaitFlag(cmd, &client.WaitStrategy) cmd.MarkFlagsMutuallyExclusive("force-replace", "force-conflicts") cmd.MarkFlagsMutuallyExclusive("force", "force-conflicts") diff --git a/pkg/cmd/template.go b/pkg/cmd/template.go index 81c112d51..af0690c57 100644 --- a/pkg/cmd/template.go +++ b/pkg/cmd/template.go @@ -67,7 +67,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { ValidArgsFunction: func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return compInstall(args, toComplete, client) }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { if kubeVersion != "" { parsedKubeVersion, err := common.ParseKubeVersion(kubeVersion) if err != nil { @@ -83,16 +83,17 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetRegistryClient(registryClient) - // This is for the case where "" is specifically passed in as a - // value. When there is no value passed in NoOptDefVal will be used - // and it is set to client. See addInstallFlags. - if client.DryRunOption == "" { - client.DryRunOption = "true" + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, true) + if err != nil { + return err + } + if validate { + // Mimic deprecated --validate flag behavior by enabling server dry run + dryRunStrategy = action.DryRunServer } - client.DryRun = true + client.DryRunStrategy = dryRunStrategy client.ReleaseName = "release-name" client.Replace = true // Skip the name check - client.ClientOnly = !validate client.APIVersions = common.VersionSet(extraAPIs) client.IncludeCRDs = includeCrds rel, err := runInstall(args, client, valueOpts, out) @@ -196,14 +197,21 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { addInstallFlags(cmd, f, client, valueOpts) f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") - f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install") + f.BoolVar(&validate, "validate", false, "deprecated") + f.MarkDeprecated("validate", "use '--dry-run=server' instead") f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output") f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output") f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall") f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion") f.StringSliceVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions (multiple can be specified)") f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.") + f.String( + "dry-run", + "client", + `simulates the operation either client-side or server-side. Must be either: "client", or "server". '--dry-run=client simulates the operation client-side only and avoids cluster connections. '--dry-run=server' simulates/validates the operation on the server, requiring cluster connectivity.`) + f.Lookup("dry-run").NoOptDefVal = "unset" bindPostRenderFlag(cmd, &client.PostRenderer, settings) + cmd.MarkFlagsMutuallyExclusive("validate", "dry-run") return cmd } diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index fcc4f9294..a9f834ebb 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -100,7 +100,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } return noMoreArgsComp() }, - RunE: func(_ *cobra.Command, args []string) error { + RunE: func(cmd *cobra.Command, args []string) error { client.Namespace = settings.Namespace() registryClient, err := newRegistryClient(client.CertFile, client.KeyFile, client.CaFile, @@ -110,12 +110,12 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetRegistryClient(registryClient) - // This is for the case where "" is specifically passed in as a - // value. When there is no value passed in NoOptDefVal will be used - // and it is set to client. See addInstallFlags. - if client.DryRunOption == "" { - client.DryRunOption = "none" + dryRunStrategy, err := cmdGetDryRunFlagStrategy(cmd, false) + if err != nil { + return err } + client.DryRunStrategy = dryRunStrategy + // Fixes #7002 - Support reading values from STDIN for `upgrade` command // Must load values AFTER determining if we have to call install so that values loaded from stdin are not read twice if client.Install { @@ -132,8 +132,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { instClient.CreateNamespace = createNamespace instClient.ChartPathOptions = client.ChartPathOptions instClient.ForceReplace = client.ForceReplace - instClient.DryRun = client.DryRun - instClient.DryRunOption = client.DryRunOption + instClient.DryRunStrategy = client.DryRunStrategy instClient.DisableHooks = client.DisableHooks instClient.SkipCRDs = client.SkipCRDs instClient.Timeout = client.Timeout @@ -183,10 +182,6 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - // Validate dry-run flag value is one of the allowed values - if err := validateDryRunOptionFlag(client.DryRunOption); err != nil { - return err - } p := getter.All(settings) vals, err := valueOpts.MergeValues(p) @@ -274,9 +269,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") - 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.ForceReplace, "force-replace", false, "force resource updates by replacement") f.BoolVar(&client.ForceReplace, "force", false, "deprecated") f.MarkDeprecated("force", "use --force-replace instead") @@ -303,6 +296,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "update dependencies if they are missing before installing the chart") f.BoolVar(&client.EnableDNS, "enable-dns", false, "enable DNS lookups when rendering templates") f.BoolVar(&client.TakeOwnership, "take-ownership", false, "if set, upgrade will ignore the check for helm annotations and take ownership of the existing resources") + addDryRunFlag(cmd) addChartPathOptionsFlags(f, &client.ChartPathOptions) addValueOptionsFlags(f, valueOpts) bindOutputFlag(cmd, &outfmt) From b5de5b1591b7617281ee889554e58d51edb6ef09 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Sat, 14 Jun 2025 21:20:44 -0700 Subject: [PATCH 041/118] chore: Cleanup additional/redundant kube client Interfaces Signed-off-by: George Jenkins --- pkg/action/hooks.go | 14 ++---- pkg/action/hooks_test.go | 5 +- pkg/action/rollback.go | 4 +- pkg/action/status.go | 34 ++++++------- pkg/action/uninstall.go | 6 +-- pkg/action/uninstall_test.go | 2 +- pkg/action/upgrade.go | 3 +- pkg/kube/client.go | 18 ++----- pkg/kube/client_test.go | 6 +-- pkg/kube/fake/failing_kube_client.go | 46 ++++++++---------- pkg/kube/fake/printer.go | 4 +- pkg/kube/interface.go | 71 ++++++++++------------------ 12 files changed, 84 insertions(+), 129 deletions(-) diff --git a/pkg/action/hooks.go b/pkg/action/hooks.go index 4808bc054..1e4fec9bd 100644 --- a/pkg/action/hooks.go +++ b/pkg/action/hooks.go @@ -154,7 +154,7 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo if err != nil { return fmt.Errorf("unable to build kubernetes object for deleting hook %s: %w", h.Path, err) } - _, errs := cfg.KubeClient.Delete(resources) + _, errs := cfg.KubeClient.Delete(resources, metav1.DeletePropagationBackground) if len(errs) > 0 { return joinErrors(errs, "; ") } @@ -223,16 +223,12 @@ func (cfg *Configuration) outputLogsByPolicy(h *release.Hook, releaseNamespace s } func (cfg *Configuration) outputContainerLogsForListOptions(namespace string, listOptions metav1.ListOptions) error { - // TODO Helm 4: Remove this check when GetPodList and OutputContainerLogsForPodList are moved from InterfaceLogs to Interface - if kubeClient, ok := cfg.KubeClient.(kube.InterfaceLogs); ok { - podList, err := kubeClient.GetPodList(namespace, listOptions) - if err != nil { - return err - } - err = kubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc) + podList, err := cfg.KubeClient.GetPodList(namespace, listOptions) + if err != nil { return err } - return nil + + return cfg.KubeClient.OutputContainerLogsForPodList(podList, namespace, cfg.HookOutputFunc) } func (cfg *Configuration) deriveNamespace(h *release.Hook, namespace string) (string, error) { diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index fb7d1b4ec..d3ca26615 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/cli-runtime/pkg/resource" @@ -259,7 +260,7 @@ func (h *HookFailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ t return nil } -func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { +func (h *HookFailingKubeClient) Delete(resources kube.ResourceList, deletionPropagation metav1.DeletionPropagation) (*kube.Result, []error) { for _, res := range resources { h.deleteRecord = append(h.deleteRecord, resource.Info{ Name: res.Name, @@ -267,7 +268,7 @@ func (h *HookFailingKubeClient) Delete(resources kube.ResourceList) (*kube.Resul }) } - return h.PrintingKubeClient.Delete(resources) + return h.PrintingKubeClient.Delete(resources, deletionPropagation) } func (h *HookFailingKubeClient) GetWaiter(strategy kube.WaitStrategy) (kube.Waiter, error) { diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index f56052988..3ec3680db 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -23,6 +23,8 @@ import ( "strings" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" release "helm.sh/helm/v4/pkg/release/v1" @@ -222,7 +224,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas r.cfg.recordRelease(targetRelease) if r.CleanupOnFail { slog.Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created)) - _, errs := r.cfg.KubeClient.Delete(results.Created) + _, errs := r.cfg.KubeClient.Delete(results.Created, metav1.DeletePropagationBackground) if errs != nil { return targetRelease, fmt.Errorf( "an error occurred while cleaning up resources. original rollback error: %w", diff --git a/pkg/action/status.go b/pkg/action/status.go index 509c52cd9..f3ccfe8bf 100644 --- a/pkg/action/status.go +++ b/pkg/action/status.go @@ -18,7 +18,6 @@ package action import ( "bytes" - "errors" "helm.sh/helm/v4/pkg/kube" release "helm.sh/helm/v4/pkg/release/v1" @@ -55,28 +54,25 @@ func (s *Status) Run(name string) (*release.Release, error) { return nil, err } - if kubeClient, ok := s.cfg.KubeClient.(kube.InterfaceResources); ok { - var resources kube.ResourceList - if s.ShowResourcesTable { - resources, err = kubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false) - if err != nil { - return nil, err - } - } else { - resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false) - if err != nil { - return nil, err - } + var resources kube.ResourceList + if s.ShowResourcesTable { + resources, err = s.cfg.KubeClient.BuildTable(bytes.NewBufferString(rel.Manifest), false) + if err != nil { + return nil, err } - - resp, err := kubeClient.Get(resources, true) + } else { + resources, err = s.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), false) if err != nil { return nil, err } + } - rel.Info.Resources = resp - - return rel, nil + resp, err := s.cfg.KubeClient.Get(resources, true) + if err != nil { + return nil, err } - return nil, errors.New("unable to get kubeClient with interface InterfaceResources") + + rel.Info.Resources = resp + + return rel, nil } diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 057c2118f..e3f0ecd9e 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -245,11 +245,7 @@ func (u *Uninstall) deleteRelease(rel *release.Release) (kube.ResourceList, stri return nil, "", []error{fmt.Errorf("unable to build kubernetes objects for delete: %w", err)} } if len(resources) > 0 { - if kubeClient, ok := u.cfg.KubeClient.(kube.InterfaceDeletionPropagation); ok { - _, errs = kubeClient.DeleteWithPropagationPolicy(resources, parseCascadingFlag(u.DeletionPropagation)) - return resources, kept, errs - } - _, errs = u.cfg.KubeClient.Delete(resources) + _, errs = u.cfg.KubeClient.Delete(resources, parseCascadingFlag(u.DeletionPropagation)) } return resources, kept, errs } diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 7c7344383..5f3db7c9f 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -146,7 +146,7 @@ func TestUninstallRelease_Cascade(t *testing.T) { }` unAction.cfg.Releases.Create(rel) failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) - failer.DeleteWithPropagationError = fmt.Errorf("Uninstall with cascade failed") + failer.DeleteError = fmt.Errorf("Uninstall with cascade failed") failer.BuildDummy = true unAction.cfg.KubeClient = failer _, err := unAction.Run(rel.Name) diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3c84570b2..329612649 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -26,6 +26,7 @@ import ( "sync" "time" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/resource" "helm.sh/helm/v4/pkg/chart" @@ -524,7 +525,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e u.cfg.recordRelease(rel) if u.CleanupOnFail && len(created) > 0 { slog.Debug("cleanup on fail set", "cleaning_resources", len(created)) - _, errs := u.cfg.KubeClient.Delete(created) + _, errs := u.cfg.KubeClient.Delete(created, metav1.DeletePropagationBackground) if errs != nil { return rel, fmt.Errorf( "an error occurred while cleaning up resources. original upgrade error: %w: %w", diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 26ba7abfc..a1030a316 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -86,6 +86,8 @@ type Client struct { kubeClient kubernetes.Interface } +var _ Interface = (*Client)(nil) + type WaitStrategy string const ( @@ -773,25 +775,13 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate // background cascade deletion. It will attempt to delete all resources even // if one or more fail and collect any errors. All successfully deleted items // will be returned in the `Deleted` ResourceList that is part of the result. -func (c *Client) Delete(resources ResourceList) (*Result, []error) { - return deleteResources(resources, metav1.DeletePropagationBackground) -} - -// Delete deletes Kubernetes resources specified in the resources list with -// given deletion propagation policy. It will attempt to delete all resources even -// if one or more fail and collect any errors. All successfully deleted items -// will be returned in the `Deleted` ResourceList that is part of the result. -func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) { - return deleteResources(resources, policy) -} - -func deleteResources(resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) { +func (c *Client) Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) { var errs []error res := &Result{} mtx := sync.Mutex{} err := perform(resources, func(target *resource.Info) error { slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind) - err := deleteResource(target, propagation) + err := deleteResource(target, policy) if err == nil || apierrors.IsNotFound(err) { if err != nil { slog.Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index a8a8668c7..f8e0ace9d 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -816,7 +816,7 @@ func TestWaitDelete(t *testing.T) { if len(result.Created) != 1 { t.Errorf("expected 1 resource created, got %d", len(result.Created)) } - if _, err := c.Delete(resources); err != nil { + if _, err := c.Delete(resources, metav1.DeletePropagationBackground); err != nil { t.Fatal(err) } @@ -855,7 +855,7 @@ func TestReal(t *testing.T) { t.Fatal(err) } - if _, errs := c.Delete(resources); errs != nil { + if _, errs := c.Delete(resources, metav1.DeletePropagationBackground); errs != nil { t.Fatal(errs) } @@ -864,7 +864,7 @@ func TestReal(t *testing.T) { t.Fatal(err) } // ensures that delete does not fail if a resource is not found - if _, errs := c.Delete(resources); errs != nil { + if _, errs := c.Delete(resources, metav1.DeletePropagationBackground); errs != nil { t.Fatal(errs) } } diff --git a/pkg/kube/fake/failing_kube_client.go b/pkg/kube/fake/failing_kube_client.go index 154419ebf..f340c045f 100644 --- a/pkg/kube/fake/failing_kube_client.go +++ b/pkg/kube/fake/failing_kube_client.go @@ -33,22 +33,23 @@ import ( // delegates all its calls to `PrintingKubeClient` type FailingKubeClient struct { PrintingKubeClient - CreateError error - GetError error - DeleteError error - DeleteWithPropagationError error - UpdateError error - BuildError error - BuildTableError error - ConnectionError error - BuildDummy bool - DummyResources kube.ResourceList - BuildUnstructuredError error - WaitError error - WaitForDeleteError error - WatchUntilReadyError error - WaitDuration time.Duration -} + CreateError error + GetError error + DeleteError error + UpdateError error + BuildError error + BuildTableError error + ConnectionError error + BuildDummy bool + DummyResources kube.ResourceList + BuildUnstructuredError error + WaitError error + WaitForDeleteError error + WatchUntilReadyError error + WaitDuration time.Duration +} + +var _ kube.Interface = &FailingKubeClient{} // FailingKubeWaiter implements kube.Waiter for testing purposes. // It also has additional errors you can set to fail different functions, otherwise it delegates all its calls to `PrintingKubeWaiter` @@ -102,11 +103,12 @@ func (f *FailingKubeWaiter) WaitForDelete(resources kube.ResourceList, d time.Du } // Delete returns the configured error if set or prints -func (f *FailingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { +func (f *FailingKubeClient) Delete(resources kube.ResourceList, deletionPropagation metav1.DeletionPropagation) (*kube.Result, []error) { if f.DeleteError != nil { return nil, []error{f.DeleteError} } - return f.PrintingKubeClient.Delete(resources) + + return f.PrintingKubeClient.Delete(resources, deletionPropagation) } // WatchUntilReady returns the configured error if set or prints @@ -147,14 +149,6 @@ func (f *FailingKubeClient) BuildTable(r io.Reader, _ bool) (kube.ResourceList, return f.PrintingKubeClient.BuildTable(r, false) } -// DeleteWithPropagationPolicy returns the configured error if set or prints -func (f *FailingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) { - if f.DeleteWithPropagationError != nil { - return nil, []error{f.DeleteWithPropagationError} - } - return f.PrintingKubeClient.DeleteWithPropagationPolicy(resources, policy) -} - func (f *FailingKubeClient) GetWaiter(ws kube.WaitStrategy) (kube.Waiter, error) { waiter, _ := f.PrintingKubeClient.GetWaiter(ws) printingKubeWaiter, _ := waiter.(*PrintingKubeWaiter) diff --git a/pkg/kube/fake/printer.go b/pkg/kube/fake/printer.go index 16c93615a..a7aad1dac 100644 --- a/pkg/kube/fake/printer.go +++ b/pkg/kube/fake/printer.go @@ -43,6 +43,8 @@ type PrintingKubeWaiter struct { LogOutput io.Writer } +var _ kube.Interface = &PrintingKubeClient{} + // IsReachable checks if the cluster is reachable func (p *PrintingKubeClient) IsReachable() error { return nil @@ -89,7 +91,7 @@ func (p *PrintingKubeWaiter) WatchUntilReady(resources kube.ResourceList, _ time // Delete implements KubeClient delete. // // It only prints out the content to be deleted. -func (p *PrintingKubeClient) Delete(resources kube.ResourceList) (*kube.Result, []error) { +func (p *PrintingKubeClient) Delete(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) { _, err := io.Copy(p.Out, bufferize(resources)) if err != nil { return nil, []error{err} diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index 7339ae0ff..89bf08163 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -29,11 +29,17 @@ import ( // // A KubernetesClient must be concurrency safe. type Interface interface { + // Get details of deployed resources. + // The first argument is a list of resources to get. The second argument + // specifies if related pods should be fetched. For example, the pods being + // managed by a deployment. + Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) + // Create creates one or more resources. Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) // Delete destroys one or more resources. - Delete(resources ResourceList) (*Result, []error) + Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) // Update updates one or more resources or creates the resource // if it doesn't exist. @@ -51,6 +57,23 @@ type Interface interface { // Get Waiter gets the Kube.Waiter GetWaiter(ws WaitStrategy) (Waiter, error) + + // GetPodList list all pods that match the specified listOptions + GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) + + // OutputContainerLogsForPodList output the logs for a pod list + OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error + + // BuildTable creates a resource list from a Reader. This differs from + // Interface.Build() in that a table kind is returned. A table is useful + // if you want to use a printer to display the information. + // + // Reader must contain a YAML stream (one or more YAML documents separated + // by "\n---\n") + // + // Validates against OpenAPI schema if validate is true. + // TODO Helm 4: Integrate into Build with an argument + BuildTable(reader io.Reader, validate bool) (ResourceList, error) } // Waiter defines methods related to waiting for resource states. @@ -75,49 +98,3 @@ type Waiter interface { // error. WatchUntilReady(resources ResourceList, timeout time.Duration) error } - -// InterfaceLogs was introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceLogs and integrate its method(s) into the Interface. -type InterfaceLogs interface { - // GetPodList list all pods that match the specified listOptions - GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) - - // OutputContainerLogsForPodList output the logs for a pod list - OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error -} - -// InterfaceDeletionPropagation is introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceDeletionPropagation and integrate its method(s) into the Interface. -type InterfaceDeletionPropagation interface { - // DeleteWithPropagationPolicy destroys one or more resources. The deletion propagation is handled as per the given deletion propagation value. - DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) -} - -// InterfaceResources is introduced to avoid breaking backwards compatibility for Interface implementers. -// -// TODO Helm 4: Remove InterfaceResources and integrate its method(s) into the Interface. -type InterfaceResources interface { - // Get details of deployed resources. - // The first argument is a list of resources to get. The second argument - // specifies if related pods should be fetched. For example, the pods being - // managed by a deployment. - Get(resources ResourceList, related bool) (map[string][]runtime.Object, error) - - // BuildTable creates a resource list from a Reader. This differs from - // Interface.Build() in that a table kind is returned. A table is useful - // if you want to use a printer to display the information. - // - // Reader must contain a YAML stream (one or more YAML documents separated - // by "\n---\n") - // - // Validates against OpenAPI schema if validate is true. - // TODO Helm 4: Integrate into Build with an argument - BuildTable(reader io.Reader, validate bool) (ResourceList, error) -} - -var _ Interface = (*Client)(nil) -var _ InterfaceLogs = (*Client)(nil) -var _ InterfaceDeletionPropagation = (*Client)(nil) -var _ InterfaceResources = (*Client)(nil) From efcb5735e5cba163dd4acfb1a3489bff1b70f49a Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Mon, 31 Mar 2025 09:09:51 -0700 Subject: [PATCH 042/118] cleanup: Remove extra lint/rules.Template functions Signed-off-by: George Jenkins --- pkg/chart/v2/lint/lint.go | 7 +- pkg/chart/v2/lint/rules/template.go | 91 ++++++++++++++++-------- pkg/chart/v2/lint/rules/template_test.go | 37 ++++++++-- 3 files changed, 97 insertions(+), 38 deletions(-) diff --git a/pkg/chart/v2/lint/lint.go b/pkg/chart/v2/lint/lint.go index b26d65a34..1c871d936 100644 --- a/pkg/chart/v2/lint/lint.go +++ b/pkg/chart/v2/lint/lint.go @@ -58,7 +58,12 @@ func RunAll(baseDir string, values map[string]interface{}, namespace string, opt rules.Chartfile(&result) rules.ValuesWithOverrides(&result, values, lo.SkipSchemaValidation) - rules.TemplatesWithSkipSchemaValidation(&result, values, namespace, lo.KubeVersion, lo.SkipSchemaValidation) + rules.Templates( + &result, + namespace, + values, + rules.TemplateLinterKubeVersion(lo.KubeVersion), + rules.TemplateLinterSkipSchemaValidation(lo.SkipSchemaValidation)) rules.Dependencies(&result) rules.Crds(&result) diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 5c84d0f68..b21050a9e 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -42,35 +42,66 @@ import ( ) // Templates lints the templates in the Linter. -func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) { - TemplatesWithKubeVersion(linter, values, namespace, nil) +func Templates(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) { + templateLinter := newTemplateLinter(linter, namespace, values, options...) + templateLinter.Lint() } -// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version. -func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion) { - TemplatesWithSkipSchemaValidation(linter, values, namespace, kubeVersion, false) +type TemplateLinterOption func(*templateLinter) + +func TemplateLinterKubeVersion(kubeVersion *common.KubeVersion) TemplateLinterOption { + return func(tl *templateLinter) { + tl.kubeVersion = kubeVersion + } +} + +func TemplateLinterSkipSchemaValidation(skipSchemaValidation bool) TemplateLinterOption { + return func(tl *templateLinter) { + tl.skipSchemaValidation = skipSchemaValidation + } } -// TemplatesWithSkipSchemaValidation lints the templates in the Linter, allowing to specify the kubernetes version and if schema validation is enabled or not. -func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *common.KubeVersion, skipSchemaValidation bool) { - fpath := "templates/" - templatesPath := filepath.Join(linter.ChartDir, fpath) +func newTemplateLinter(linter *support.Linter, namespace string, values map[string]any, options ...TemplateLinterOption) templateLinter { + + result := templateLinter{ + linter: linter, + values: values, + namespace: namespace, + } - // Templates directory is optional for now - templatesDirExists := linter.RunLinterRule(support.WarningSev, fpath, templatesDirExists(templatesPath)) + for _, o := range options { + o(&result) + } + + return result +} + +type templateLinter struct { + linter *support.Linter + values map[string]any + namespace string + kubeVersion *common.KubeVersion + skipSchemaValidation bool +} + +func (t *templateLinter) Lint() { + templatesDir := "templates/" + templatesPath := filepath.Join(t.linter.ChartDir, templatesDir) + + templatesDirExists := t.linter.RunLinterRule(support.WarningSev, templatesDir, templatesDirExists(templatesPath)) if !templatesDirExists { return } - validTemplatesDir := linter.RunLinterRule(support.ErrorSev, fpath, validateTemplatesDir(templatesPath)) + validTemplatesDir := t.linter.RunLinterRule(support.ErrorSev, templatesDir, validateTemplatesDir(templatesPath)) if !validTemplatesDir { return } // Load chart and parse templates - chart, err := loader.Load(linter.ChartDir) + chart, err := loader.Load(t.linter.ChartDir) - chartLoaded := linter.RunLinterRule(support.ErrorSev, fpath, err) + chartLoaded := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) if !chartLoaded { return @@ -78,35 +109,35 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string options := common.ReleaseOptions{ Name: "test-release", - Namespace: namespace, + Namespace: t.namespace, } caps := common.DefaultCapabilities.Copy() - if kubeVersion != nil { - caps.KubeVersion = *kubeVersion + if t.kubeVersion != nil { + caps.KubeVersion = *t.kubeVersion } // lint ignores import-values // See https://github.com/helm/helm/issues/9658 - if err := chartutil.ProcessDependencies(chart, values); err != nil { + if err := chartutil.ProcessDependencies(chart, t.values); err != nil { return } - cvals, err := util.CoalesceValues(chart, values) + cvals, err := util.CoalesceValues(chart, t.values) if err != nil { return } - valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, skipSchemaValidation) + valuesToRender, err := util.ToRenderValuesWithSchemaValidation(chart, cvals, options, caps, t.skipSchemaValidation) if err != nil { - linter.RunLinterRule(support.ErrorSev, fpath, err) + t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) return } var e engine.Engine e.LintMode = true renderedContentMap, err := e.Render(chart, valuesToRender) - renderOk := linter.RunLinterRule(support.ErrorSev, fpath, err) + renderOk := t.linter.RunLinterRule(support.ErrorSev, templatesDir, err) if !renderOk { return @@ -121,9 +152,8 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string */ for _, template := range chart.Templates { fileName := template.Name - fpath = fileName - linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) + t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName)) // We only apply the following lint rules to yaml files if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { @@ -139,7 +169,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string renderedContent := renderedContentMap[path.Join(chart.Name(), fileName)] if strings.TrimSpace(renderedContent) != "" { - linter.RunLinterRule(support.WarningSev, fpath, validateTopIndentLevel(renderedContent)) + t.linter.RunLinterRule(support.WarningSev, fileName, validateTopIndentLevel(renderedContent)) decoder := yaml.NewYAMLOrJSONDecoder(strings.NewReader(renderedContent), 4096) @@ -156,17 +186,17 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string // If YAML linting fails here, it will always fail in the next block as well, so we should return here. // fix https://github.com/helm/helm/issues/11391 - if !linter.RunLinterRule(support.ErrorSev, fpath, validateYamlContent(err)) { + if !t.linter.RunLinterRule(support.ErrorSev, fileName, validateYamlContent(err)) { return } if yamlStruct != nil { // NOTE: set to warnings to allow users to support out-of-date kubernetes // Refs https://github.com/helm/helm/issues/8596 - linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct)) - linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion)) + t.linter.RunLinterRule(support.WarningSev, fileName, validateMetadataName(yamlStruct)) + t.linter.RunLinterRule(support.WarningSev, fileName, validateNoDeprecations(yamlStruct, t.kubeVersion)) - linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent)) - linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent)) + t.linter.RunLinterRule(support.ErrorSev, fileName, validateMatchSelector(yamlStruct, renderedContent)) + t.linter.RunLinterRule(support.ErrorSev, fileName, validateListAnnotations(yamlStruct, renderedContent)) } } } @@ -234,6 +264,7 @@ func validateYamlContent(err error) error { if err != nil { return fmt.Errorf("unable to parse YAML: %w", err) } + return nil } diff --git a/pkg/chart/v2/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go index 3e8e0b371..7629d3de5 100644 --- a/pkg/chart/v2/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -51,11 +51,14 @@ func TestValidateAllowedExtension(t *testing.T) { var values = map[string]interface{}{"nameOverride": "", "httpPort": 80} const namespace = "testNamespace" -const strict = false func TestTemplateParsing(t *testing.T) { linter := support.Linter{ChartDir: templateTestBasedir} - Templates(&linter, values, namespace, strict) + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) res := linter.Messages if len(res) != 1 { @@ -78,7 +81,11 @@ func TestTemplateIntegrationHappyPath(t *testing.T) { defer os.Rename(ignoredTemplatePath, wrongTemplatePath) linter := support.Linter{ChartDir: templateTestBasedir} - Templates(&linter, values, namespace, strict) + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) res := linter.Messages if len(res) != 0 { @@ -88,7 +95,11 @@ func TestTemplateIntegrationHappyPath(t *testing.T) { func TestMultiTemplateFail(t *testing.T) { linter := support.Linter{ChartDir: "./testdata/multi-template-fail"} - Templates(&linter, values, namespace, strict) + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) res := linter.Messages if len(res) != 1 { @@ -208,7 +219,11 @@ func TestDeprecatedAPIFails(t *testing.T) { } linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} - Templates(&linter, values, namespace, strict) + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) if l := len(linter.Messages); l != 1 { for i, msg := range linter.Messages { t.Logf("Message %d: %s", i, msg) @@ -264,7 +279,11 @@ func TestStrictTemplateParsingMapError(t *testing.T) { linter := &support.Linter{ ChartDir: filepath.Join(dir, ch.Metadata.Name), } - Templates(linter, ch.Values, namespace, strict) + Templates( + linter, + namespace, + ch.Values, + TemplateLinterSkipSchemaValidation(false)) if len(linter.Messages) != 0 { t.Errorf("expected zero messages, got %d", len(linter.Messages)) for i, msg := range linter.Messages { @@ -393,7 +412,11 @@ func TestEmptyWithCommentsManifests(t *testing.T) { } linter := support.Linter{ChartDir: filepath.Join(tmpdir, mychart.Name())} - Templates(&linter, values, namespace, strict) + Templates( + &linter, + namespace, + values, + TemplateLinterSkipSchemaValidation(false)) if l := len(linter.Messages); l > 0 { for i, msg := range linter.Messages { t.Logf("Message %d: %s", i, msg) From 34c2d5c1a9c9c3c2e7e1311286cc3b84a4f59781 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Mon, 22 Sep 2025 23:19:07 +0200 Subject: [PATCH 043/118] Remove unused golangci-lint rules that produce warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid this noisy output when running local linting: ```sh ❯ golangci-lint run ./... WARN [runner/exclusion_rules] Skipped 0 issues by rules: [Text: "var-naming: avoid meaningless package names", Linters: "revive"] ``` Signed-off-by: Benoit Tigeot --- .golangci.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 3df31b997..bf19e2838 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -42,11 +42,6 @@ linters: - legacy - std-error-handling - rules: - - linters: - - revive - text: 'var-naming: avoid meaningless package names' - warn-unused: true settings: From 7c9176ae350ee73a1316d0d65722ef433df54477 Mon Sep 17 00:00:00 2001 From: Siew Kam Onn Date: Fri, 19 Sep 2025 11:51:22 +0800 Subject: [PATCH 044/118] Update dependencies and refactor crypto imports to use ProtonMail's go-crypto package Signed-off-by: Siew Kam Onn --- go.mod | 2 + go.sum | 4 ++ internal/plugin/signing_info.go | 2 +- pkg/provenance/sign.go | 9 +-- pkg/provenance/sign_test.go | 58 +++++++++++++++++- .../testdata/helm-mixed-keyring.pub | Bin 0 -> 1493 bytes 6 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 pkg/provenance/testdata/helm-mixed-keyring.pub diff --git a/go.mod b/go.mod index 77e761de2..50e736d20 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/Masterminds/sprig/v3 v3.3.0 github.com/Masterminds/squirrel v1.5.4 github.com/Masterminds/vcs v1.13.3 + github.com/ProtonMail/go-crypto v1.3.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 github.com/cyphar/filepath-securejoin v0.4.1 github.com/distribution/distribution/v3 v3.0.0 @@ -64,6 +65,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/cloudflare/circl v1.6.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index 9fa40e4d4..6ea802640 100644 --- a/go.sum +++ b/go.sum @@ -22,6 +22,8 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gweE= github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -49,6 +51,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= +github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= diff --git a/internal/plugin/signing_info.go b/internal/plugin/signing_info.go index 43d01c893..61ee9cd15 100644 --- a/internal/plugin/signing_info.go +++ b/internal/plugin/signing_info.go @@ -23,7 +23,7 @@ import ( "path/filepath" "strings" - "golang.org/x/crypto/openpgp/clearsign" //nolint + "github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint "helm.sh/helm/v4/pkg/helmpath" ) diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 3ffad2765..57af1ad42 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -25,9 +25,9 @@ import ( "os" "strings" - "golang.org/x/crypto/openpgp" //nolint - "golang.org/x/crypto/openpgp/clearsign" //nolint - "golang.org/x/crypto/openpgp/packet" //nolint + "github.com/ProtonMail/go-crypto/openpgp" //nolint + "github.com/ProtonMail/go-crypto/openpgp/clearsign" //nolint + "github.com/ProtonMail/go-crypto/openpgp/packet" //nolint "sigs.k8s.io/yaml" ) @@ -281,8 +281,9 @@ func (s *Signatory) Verify(archiveData, provData []byte, filename string) (*Veri func (s *Signatory) verifySignature(block *clearsign.Block) (*openpgp.Entity, error) { return openpgp.CheckDetachedSignature( s.KeyRing, - bytes.NewBuffer(block.Bytes), + bytes.NewReader(block.Bytes), block.ArmoredSignature.Body, + &defaultPGPConfig, ) } diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 4f2fc7298..1efa62819 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -24,7 +24,8 @@ import ( "strings" "testing" - pgperrors "golang.org/x/crypto/openpgp/errors" //nolint + pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" //nolint + "github.com/ProtonMail/go-crypto/openpgp/packet" //nolint "sigs.k8s.io/yaml" "helm.sh/helm/v4/pkg/chart/v2/loader" @@ -59,6 +60,9 @@ const ( // testTamperedSigBlock is a tampered copy of msgblock.yaml.asc testTamperedSigBlock = "testdata/msgblock.yaml.tampered" + // testMixedKeyring points to a keyring containing RSA and ed25519 keys. + testMixedKeyring = "testdata/helm-mixed-keyring.pub" + // testSumfile points to a SHA256 sum generated by an external tool. // We always want to validate against an external tool's representation to // verify that we haven't done something stupid. This file was generated @@ -266,6 +270,58 @@ func TestClearSign(t *testing.T) { } } +func TestMixedKeyringRSASigningAndVerification(t *testing.T) { + signer, err := NewFromFiles(testKeyfile, testMixedKeyring) + if err != nil { + t.Fatal(err) + } + + if signer.Entity == nil { + t.Fatal("expected signer entity to be loaded") + } + + if signer.Entity.PrivateKey == nil { + t.Fatal("expected signer private key to be loaded") + } + + if signer.Entity.PrivateKey.PubKeyAlgo != packet.PubKeyAlgoRSA { + t.Fatalf("expected RSA key but got %v", signer.Entity.PrivateKey.PubKeyAlgo) + } + + metadataBytes := loadChartMetadataForSigning(t, testChartfile) + + archiveData, err := os.ReadFile(testChartfile) + if err != nil { + t.Fatal(err) + } + + sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes) + if err != nil { + t.Fatalf("failed to sign chart: %v", err) + } + + verification, err := signer.Verify(archiveData, []byte(sig), filepath.Base(testChartfile)) + if err != nil { + t.Fatalf("failed to verify chart signature: %v", err) + } + + if verification.SignedBy == nil { + t.Fatal("expected verification to include signer") + } + + if verification.SignedBy.PrimaryKey == nil { + t.Fatal("expected verification to include signer primary key") + } + + if verification.SignedBy.PrimaryKey.PubKeyAlgo != packet.PubKeyAlgoRSA { + t.Fatalf("expected verification to report RSA key but got %v", verification.SignedBy.PrimaryKey.PubKeyAlgo) + } + + if _, ok := verification.SignedBy.Identities[testKeyName]; !ok { + t.Fatalf("expected verification to be signed by %q", testKeyName) + } +} + // failSigner always fails to sign and returns an error type failSigner struct{} diff --git a/pkg/provenance/testdata/helm-mixed-keyring.pub b/pkg/provenance/testdata/helm-mixed-keyring.pub new file mode 100644 index 0000000000000000000000000000000000000000..7985bd20f1b2390915c75aff4a24c61c77145fba GIT binary patch literal 1493 zcmYk6c{tQ*9Ke6SF~-c0NopvTab@Hxj3dP)MH9Xny(O5%q&qu zVL#4!o6%*gtn{TBmF0={cdAn0jK>Sr4{lEy7G9I0zFjMQNx$`r^J7Ab33WuKdUQH# zs>lCDLoyt1W*(qrBD=Zu^5ynSdAB|H!@L#Vv@G^Dk2(+^&NNg%GM8R8<<;u6IhD&4 zvfYzlpVR*{FAbFAOwQ);<%J1H1r7hlEN@lav+UD!~|ZVgkii$7#fvC#P|eW#RPq=RKplsW2~$R7=oSsd4ie} z<^=i69Mx}G{LVv-N;XIWb)cdk5|H7C>|7lp!N)I%fD7>PA3!4Dg8XO*9|%CCK!{ir zfJE$#({Im{?o9AXGJyk4YqP^s95Igm_1w%KBq}@TW}C7P8P}c39JZ^3y<}Lucj|~s zq+bH7kK5MjOvs>$JGp2V;a7VUi#>%G-NJfx6%Ph5Ki77{SZk$5swd~+sxWWMGe=bu zmKg&za2MiwpE@kUS*dPCk&diBck+4T0~TaK?)rJSf?7o}TRge1wINiRJa1~fmX1t} zxk+vgT;;FhPUze^Fd~pKWZhOzv7c(mRk564F~>XaG}vLNmJfa$AB$9JYF?CYFlpQ} zZa!ai(vG`gv!MiIOt=SE6l6UX@2srKjde%z{uKE^rOsS|bYD(cXX+DeIoR#=qS^5^Emos61*SEV-0-J;N@NXC$ zXY(r8wP<59GP|HAfznX%FC-zplO*(iV#aWAAaNp9M=yCG9eB^;^e zn7mZi6OcSSJosqYKZS>d$(!~bV$cYW&Wf_jCWoen>u5`Jd%Jpyh&Udu&05&feRK9$ zFZyXP)T_{!$x|GMu2QD@>u2(^(+`eJP%ir&J#?9zso zV)#Ks^A-PR3ha-jB^R$HkHpNNI*L+QaLt^n(eX;UB{iZ@S$sp=`&jKpfa&-24_-Vs ziJ}>!2|vv_YJ5aKx{;)BW7U)OVI(#=&a@~1uYd}L_3w2cz8PL6-HLm6Aw%mGA*}O> zM}3fO$?3a1hc3A?z|Me{LGOTMEC=OjLdnku$Iu967zhQqW+)I8z`GlL!d9?-Vh?F9 zBs8?3+y7e1YpK$Z#TtDI7Mq^vx!;7{_1}c3fx{is+x-W=sZ9DB`d8(|e5fcI37kU9 zKrP#7mI_Wnl!UYyk@a};KDK&4ksq?Nd`EBue|bUFuI3PkECjY|It03-Iz&p~o9W-X zn$WjEHt5U^W$cO!`t~&PZY)|P96ki0GIa68*0yXCKH?nj>gNd_*5Q!n+ V@cIdPI!rgbCMSw~vn_HR_y>WtmqP#m literal 0 HcmV?d00001 From 0b5d8033b98a61675d9820f8fb999ae5bf7ddbc4 Mon Sep 17 00:00:00 2001 From: Siew Kam Onn Date: Fri, 19 Sep 2025 13:51:01 +0800 Subject: [PATCH 045/118] Add Ed25519 key presence check in mixed keyring signing test Signed-off-by: Siew Kam Onn --- pkg/provenance/sign_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index 1efa62819..db43779dc 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -276,6 +276,33 @@ func TestMixedKeyringRSASigningAndVerification(t *testing.T) { t.Fatal(err) } + if len(signer.KeyRing) == 0 { + t.Fatal("expected signer keyring to be loaded") + } + + hasEdDSA := false + for _, entity := range signer.KeyRing { + if entity.PrimaryKey != nil && entity.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA { + hasEdDSA = true + break + } + + for _, subkey := range entity.Subkeys { + if subkey.PublicKey != nil && subkey.PublicKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA { + hasEdDSA = true + break + } + } + + if hasEdDSA { + break + } + } + + if !hasEdDSA { + t.Fatalf("expected %s to include an Ed25519 public key", testMixedKeyring) + } + if signer.Entity == nil { t.Fatal("expected signer entity to be loaded") } From 1c8d5804e232c338959b151c952f59f1725961a4 Mon Sep 17 00:00:00 2001 From: Siew Kam Onn Date: Tue, 23 Sep 2025 16:00:04 +0800 Subject: [PATCH 046/118] Refactor SQL tests to use recentUnixTimestamp for time arguments Signed-off-by: Siew Kam Onn --- pkg/storage/driver/sql_test.go | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/pkg/storage/driver/sql_test.go b/pkg/storage/driver/sql_test.go index bd2918aad..fe147816c 100644 --- a/pkg/storage/driver/sql_test.go +++ b/pkg/storage/driver/sql_test.go @@ -14,6 +14,7 @@ limitations under the License. package driver import ( + "database/sql/driver" "fmt" "reflect" "regexp" @@ -26,6 +27,33 @@ import ( rspb "helm.sh/helm/v4/pkg/release/v1" ) +const recentTimestampTolerance = time.Second + +func recentUnixTimestamp() sqlmock.Argument { + return recentUnixTimestampArgument{} +} + +type recentUnixTimestampArgument struct{} + +func (recentUnixTimestampArgument) Match(value driver.Value) bool { + var ts int64 + switch v := value.(type) { + case int: + ts = int64(v) + case int64: + ts = v + default: + return false + } + + diff := time.Since(time.Unix(ts, 0)) + if diff < 0 { + diff = -diff + } + + return diff <= recentTimestampTolerance +} + func TestSQLName(t *testing.T) { sqlDriver, _ := newTestFixtureSQL(t) if sqlDriver.Name() != SQLDriverName { @@ -197,7 +225,7 @@ func TestSqlCreate(t *testing.T) { mock.ExpectBegin() mock. ExpectExec(regexp.QuoteMeta(query)). - WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()). WillReturnResult(sqlmock.NewResult(1, 1)) labelsQuery := fmt.Sprintf( @@ -255,7 +283,7 @@ func TestSqlCreateAlreadyExists(t *testing.T) { mock.ExpectBegin() mock. ExpectExec(regexp.QuoteMeta(insertQuery)). - WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix())). + WithArgs(key, sqlReleaseDefaultType, body, rel.Name, rel.Namespace, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp()). WillReturnError(fmt.Errorf("dialect dependent SQL error")) selectQuery := fmt.Sprintf( @@ -313,7 +341,7 @@ func TestSqlUpdate(t *testing.T) { mock. ExpectExec(regexp.QuoteMeta(query)). - WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, int(time.Now().Unix()), key, namespace). + WithArgs(body, rel.Name, int(rel.Version), rel.Info.Status.String(), sqlReleaseDefaultOwner, recentUnixTimestamp(), key, namespace). WillReturnResult(sqlmock.NewResult(0, 1)) if err := sqlDriver.Update(key, rel); err != nil { From 69dbd6115e8cfb96b35bbb9221337ba8935a2650 Mon Sep 17 00:00:00 2001 From: juejinyuxitu Date: Tue, 23 Sep 2025 18:40:56 +0800 Subject: [PATCH 047/118] chore: fix some typos in comment Signed-off-by: juejinyuxitu --- internal/plugin/runtime_subprocess.go | 2 +- pkg/kube/client.go | 2 +- pkg/kube/client_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/plugin/runtime_subprocess.go b/internal/plugin/runtime_subprocess.go index 5e6676a00..802732b14 100644 --- a/internal/plugin/runtime_subprocess.go +++ b/internal/plugin/runtime_subprocess.go @@ -29,7 +29,7 @@ import ( "helm.sh/helm/v4/internal/plugin/schema" ) -// SubprocessProtocolCommand maps a given protocol to the getter command used to retrieve artifacts for that protcol +// SubprocessProtocolCommand maps a given protocol to the getter command used to retrieve artifacts for that protocol type SubprocessProtocolCommand struct { // Protocols are the list of schemes from the charts URL. Protocols []string `yaml:"protocols"` diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 26ba7abfc..ef2a72463 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1031,7 +1031,7 @@ func patchResourceClientSide(original runtime.Object, target *resource.Info, thr } // upgradeClientSideFieldManager is simply a wrapper around csaupgrade.UpgradeManagedFields -// that ugrade CSA managed fields to SSA apply +// that upgrade CSA managed fields to SSA apply // see: https://github.com/kubernetes/kubernetes/pull/112905 func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValidationDirective FieldValidationDirective) (bool, error) { diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index a8a8668c7..cd59f950c 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -142,7 +142,7 @@ func NewRequestResponseLogClient(t *testing.T, cb RoundTripperTestFunc) RequestR } // RequestResponseLogClient is a test client that logs requests and responses -// Satifying http.RoundTripper interface, it can be used to mock HTTP requests in tests. +// Satisfying http.RoundTripper interface, it can be used to mock HTTP requests in tests. // Forwarding requests to a callback function (cb) that can be used to simulate server responses. type RequestResponseLogClient struct { t *testing.T From cf811bb11f857c7af42c174e829ccf90c0f29ef3 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 8 Sep 2025 16:07:48 -0600 Subject: [PATCH 048/118] chore: registry utils clean up Signed-off-by: Terry Howe --- pkg/registry/{util.go => chart.go} | 82 ------------- pkg/registry/{util_test.go => chart_test.go} | 0 pkg/registry/reference.go | 6 + pkg/registry/tag.go | 59 +++++++++ pkg/registry/tag_test.go | 122 +++++++++++++++++++ 5 files changed, 187 insertions(+), 82 deletions(-) rename pkg/registry/{util.go => chart.go} (61%) rename pkg/registry/{util_test.go => chart_test.go} (100%) create mode 100644 pkg/registry/tag.go create mode 100644 pkg/registry/tag_test.go diff --git a/pkg/registry/util.go b/pkg/registry/chart.go similarity index 61% rename from pkg/registry/util.go rename to pkg/registry/chart.go index 6071c66c3..b00fc616d 100644 --- a/pkg/registry/util.go +++ b/pkg/registry/chart.go @@ -18,18 +18,12 @@ package registry // import "helm.sh/helm/v4/pkg/registry" import ( "bytes" - "fmt" - "io" - "net/http" - "slices" "strings" "time" - "helm.sh/helm/v4/internal/tlsutil" chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" - "github.com/Masterminds/semver/v3" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -38,52 +32,6 @@ var immutableOciAnnotations = []string{ ocispec.AnnotationTitle, } -// IsOCI determines whether a URL is to be treated as an OCI URL -func IsOCI(url string) bool { - return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) -} - -// ContainsTag determines whether a tag is found in a provided list of tags -func ContainsTag(tags []string, tag string) bool { - return slices.Contains(tags, tag) -} - -func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { - var constraint *semver.Constraints - if versionString == "" { - // If string is empty, set wildcard constraint - constraint, _ = semver.NewConstraint("*") - } else { - // when customer inputs specific version, check whether there's an exact match first - for _, v := range tags { - if versionString == v { - return v, nil - } - } - - // Otherwise set constraint to the string given - var err error - constraint, err = semver.NewConstraint(versionString) - if err != nil { - return "", err - } - } - - // Otherwise try to find the first available version matching the string, - // in case it is a constraint - for _, v := range tags { - test, err := semver.NewVersion(v) - if err != nil { - continue - } - if constraint.Check(test) { - return v, nil - } - } - - return "", fmt.Errorf("could not locate a version matching provided version string %s", versionString) -} - // extractChartMeta is used to extract a chart metadata from a byte array func extractChartMeta(chartData []byte) (*chart.Metadata, error) { ch, err := loader.LoadArchive(bytes.NewReader(chartData)) @@ -93,35 +41,6 @@ func extractChartMeta(chartData []byte) (*chart.Metadata, error) { return ch.Metadata, nil } -// NewRegistryClientWithTLS is a helper function to create a new registry client with TLS enabled. -func NewRegistryClientWithTLS(out io.Writer, certFile, keyFile, caFile string, insecureSkipTLSverify bool, registryConfig string, debug bool) (*Client, error) { - tlsConf, err := tlsutil.NewTLSConfig( - tlsutil.WithInsecureSkipVerify(insecureSkipTLSverify), - tlsutil.WithCertKeyPairFiles(certFile, keyFile), - tlsutil.WithCAFile(caFile), - ) - if err != nil { - return nil, fmt.Errorf("can't create TLS config for client: %s", err) - } - // Create a new registry client - registryClient, err := NewClient( - ClientOptDebug(debug), - ClientOptEnableCache(true), - ClientOptWriter(out), - ClientOptCredentialsFile(registryConfig), - ClientOptHTTPClient(&http.Client{ - Transport: &http.Transport{ - TLSClientConfig: tlsConf, - Proxy: http.ProxyFromEnvironment, - }, - }), - ) - if err != nil { - return nil, err - } - return registryClient, nil -} - // generateOCIAnnotations will generate OCI annotations to include within the OCI manifest func generateOCIAnnotations(meta *chart.Metadata, creationTime string) map[string]string { @@ -202,5 +121,4 @@ func addToMap(inputMap map[string]string, newKey string, newValue string) map[st } return inputMap - } diff --git a/pkg/registry/util_test.go b/pkg/registry/chart_test.go similarity index 100% rename from pkg/registry/util_test.go rename to pkg/registry/chart_test.go diff --git a/pkg/registry/reference.go b/pkg/registry/reference.go index b5677761d..bd0974e69 100644 --- a/pkg/registry/reference.go +++ b/pkg/registry/reference.go @@ -17,6 +17,7 @@ limitations under the License. package registry import ( + "fmt" "strings" "oras.land/oras-go/v2/registry" @@ -76,3 +77,8 @@ func (r *reference) String() string { } return r.orasReference.String() } + +// IsOCI determines whether a URL is to be treated as an OCI URL +func IsOCI(url string) bool { + return strings.HasPrefix(url, fmt.Sprintf("%s://", OCIScheme)) +} diff --git a/pkg/registry/tag.go b/pkg/registry/tag.go new file mode 100644 index 000000000..701701d7b --- /dev/null +++ b/pkg/registry/tag.go @@ -0,0 +1,59 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry // import "helm.sh/helm/v4/pkg/registry" + +import ( + "fmt" + + "github.com/Masterminds/semver/v3" +) + +func GetTagMatchingVersionOrConstraint(tags []string, versionString string) (string, error) { + var constraint *semver.Constraints + if versionString == "" { + // If string is empty, set wildcard constraint + constraint, _ = semver.NewConstraint("*") + } else { + // when customer inputs specific version, check whether there's an exact match first + for _, v := range tags { + if versionString == v { + return v, nil + } + } + + // Otherwise set constraint to the string given + var err error + constraint, err = semver.NewConstraint(versionString) + if err != nil { + return "", err + } + } + + // Otherwise try to find the first available version matching the string, + // in case it is a constraint + for _, v := range tags { + test, err := semver.NewVersion(v) + if err != nil { + continue + } + if constraint.Check(test) { + return v, nil + } + } + + return "", fmt.Errorf("could not locate a version matching provided version string %s", versionString) +} diff --git a/pkg/registry/tag_test.go b/pkg/registry/tag_test.go new file mode 100644 index 000000000..09f0f12ea --- /dev/null +++ b/pkg/registry/tag_test.go @@ -0,0 +1,122 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package registry + +import ( + "strings" + "testing" +) + +func TestGetTagMatchingVersionOrConstraint_ExactMatch(t *testing.T) { + tags := []string{"1.0.0", "1.2.3", "2.0.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.2.3" { + t.Fatalf("expected exact match '1.2.3', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_EmptyVersionWildcard(t *testing.T) { + // Includes a non-semver tag which should be skipped + tags := []string{"latest", "0.9.0", "1.0.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Should pick the first valid semver tag in order, which is 0.9.0 + if got != "0.9.0" { + t.Fatalf("expected '0.9.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_ConstraintRange(t *testing.T) { + tags := []string{"0.5.0", "1.0.0", "1.1.0", "2.0.0"} + + // Caret range + got, err := GetTagMatchingVersionOrConstraint(tags, "^1.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { // first match in order + t.Fatalf("expected '1.0.0', got %q", got) + } + + // Compound range + got, err = GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { + t.Fatalf("expected '1.0.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_InvalidConstraint(t *testing.T) { + tags := []string{"1.0.0"} + _, err := GetTagMatchingVersionOrConstraint(tags, ">a1") + if err == nil { + t.Fatalf("expected error for invalid constraint") + } +} + +func TestGetTagMatchingVersionOrConstraint_NoMatches(t *testing.T) { + tags := []string{"0.1.0", "0.2.0"} + _, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0") + if err == nil { + t.Fatalf("expected error when no tags match") + } + if !strings.Contains(err.Error(), ">=1.0.0") { + t.Fatalf("expected error to contain version string, got: %v", err) + } +} + +func TestGetTagMatchingVersionOrConstraint_SkipsNonSemverTags(t *testing.T) { + tags := []string{"alpha", "1.0.0", "beta", "1.1.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.0.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.0.0" { + t.Fatalf("expected '1.0.0', got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_OrderMatters_FirstMatchReturned(t *testing.T) { + // Both 1.2.0 and 1.3.0 satisfy >=1.2.0 <2.0.0, but the function returns the first in input order + tags := []string{"1.3.0", "1.2.0"} + got, err := GetTagMatchingVersionOrConstraint(tags, ">=1.2.0 <2.0.0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.3.0" { + t.Fatalf("expected '1.3.0' (first satisfying tag), got %q", got) + } +} + +func TestGetTagMatchingVersionOrConstraint_ExactMatchHasPrecedence(t *testing.T) { + // Exact match should be returned even if another earlier tag would match the parsed constraint + tags := []string{"1.3.0", "1.2.3"} + got, err := GetTagMatchingVersionOrConstraint(tags, "1.2.3") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "1.2.3" { + t.Fatalf("expected exact match '1.2.3', got %q", got) + } +} From a9a657434a4d08ab3b772f891878caaa52b7bd17 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 24 Sep 2025 15:33:15 -0600 Subject: [PATCH 049/118] fix: broken --html flag to coverage script Signed-off-by: Terry Howe --- scripts/coverage.sh | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 487d4eeee..9e85a358e 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -19,7 +19,18 @@ set -euo pipefail covermode=${COVERMODE:-atomic} coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) profile="${coverdir}/cover.out" -target="${1:-./...}" # by default the whole repository is tested +html=false +target="./..." # by default the whole repository is tested +for arg in "$@"; do + case "${arg}" in + --html) + html=true + ;; + *) + target="${arg}" + ;; + esac +done generate_cover_data() { for d in $(go list "$target"); do @@ -36,9 +47,7 @@ generate_cover_data() { generate_cover_data go tool cover -func "${profile}" -case "${1-}" in - --html) +if [ "${html}" == "true" ] ; then go tool cover -html "${profile}" - ;; -esac +fi From 9c7bf37c6fd6cadaf45803e6898166db3c7689e5 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 18 Sep 2025 13:18:27 -0600 Subject: [PATCH 050/118] fix: Ignore absolute path when RepoUrl is provided Signed-off-by: Terry Howe Co-authored-by: Mohammad Forutan --- pkg/action/install.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index c6d4f723c..404ec46a6 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -828,7 +828,7 @@ func urlEqual(u1, u2 *url.URL) bool { // This does not ensure that the chart is well-formed; only that the requested filename exists. // // Order of resolution: -// - relative to current working directory +// - relative to current working directory when --repo flag is not presented // - if path is absolute or begins with '.', error out here // - URL // @@ -841,20 +841,22 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( name = strings.TrimSpace(name) version := strings.TrimSpace(c.Version) - if _, err := os.Stat(name); err == nil { - abs, err := filepath.Abs(name) - if err != nil { - return abs, err - } - if c.Verify { - if _, err := downloader.VerifyChart(abs, abs+".prov", c.Keyring); err != nil { - return "", err + if c.RepoURL == "" { + if _, err := os.Stat(name); err == nil { + abs, err := filepath.Abs(name) + if err != nil { + return abs, err } + if c.Verify { + if _, err := downloader.VerifyChart(abs, abs+".prov", c.Keyring); err != nil { + return "", err + } + } + return abs, nil + } + if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { + return name, fmt.Errorf("path %q not found", name) } - return abs, nil - } - if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { - return name, fmt.Errorf("path %q not found", name) } dl := downloader.ChartDownloader{ From 4f86c26de1a4f49ddbd816eba6ec577c7e3bf52b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 01:32:05 +0000 Subject: [PATCH 051/118] chore(deps): bump github.com/cloudflare/circl from 1.6.0 to 1.6.1 Bumps [github.com/cloudflare/circl](https://github.com/cloudflare/circl) from 1.6.0 to 1.6.1. - [Release notes](https://github.com/cloudflare/circl/releases) - [Commits](https://github.com/cloudflare/circl/compare/v1.6.0...v1.6.1) --- updated-dependencies: - dependency-name: github.com/cloudflare/circl dependency-version: 1.6.1 dependency-type: indirect ... 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 50e736d20..c08dabc3b 100644 --- a/go.mod +++ b/go.mod @@ -65,7 +65,7 @@ require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/cloudflare/circl v1.6.0 // indirect + github.com/cloudflare/circl v1.6.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect diff --git a/go.sum b/go.sum index 6ea802640..55a63cc8c 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk= -github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= From f9152dc048bc1a57eeaa93a3de8853f73db7b3e2 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 24 Sep 2025 13:29:29 -0600 Subject: [PATCH 052/118] chore: update sign tests to use testify Signed-off-by: Terry Howe --- pkg/provenance/sign_test.go | 59 ++++++++++--------------------------- 1 file changed, 16 insertions(+), 43 deletions(-) diff --git a/pkg/provenance/sign_test.go b/pkg/provenance/sign_test.go index db43779dc..1985e9eea 100644 --- a/pkg/provenance/sign_test.go +++ b/pkg/provenance/sign_test.go @@ -26,6 +26,8 @@ import ( pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" //nolint "github.com/ProtonMail/go-crypto/openpgp/packet" //nolint + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "sigs.k8s.io/yaml" "helm.sh/helm/v4/pkg/chart/v2/loader" @@ -272,13 +274,9 @@ func TestClearSign(t *testing.T) { func TestMixedKeyringRSASigningAndVerification(t *testing.T) { signer, err := NewFromFiles(testKeyfile, testMixedKeyring) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if len(signer.KeyRing) == 0 { - t.Fatal("expected signer keyring to be loaded") - } + require.NotEmpty(t, signer.KeyRing, "expected signer keyring to be loaded") hasEdDSA := false for _, entity := range signer.KeyRing { @@ -299,54 +297,29 @@ func TestMixedKeyringRSASigningAndVerification(t *testing.T) { } } - if !hasEdDSA { - t.Fatalf("expected %s to include an Ed25519 public key", testMixedKeyring) - } - - if signer.Entity == nil { - t.Fatal("expected signer entity to be loaded") - } - - if signer.Entity.PrivateKey == nil { - t.Fatal("expected signer private key to be loaded") - } + assert.True(t, hasEdDSA, "expected %s to include an Ed25519 public key", testMixedKeyring) - if signer.Entity.PrivateKey.PubKeyAlgo != packet.PubKeyAlgoRSA { - t.Fatalf("expected RSA key but got %v", signer.Entity.PrivateKey.PubKeyAlgo) - } + require.NotNil(t, signer.Entity, "expected signer entity to be loaded") + require.NotNil(t, signer.Entity.PrivateKey, "expected signer private key to be loaded") + assert.Equal(t, packet.PubKeyAlgoRSA, signer.Entity.PrivateKey.PubKeyAlgo, "expected RSA key") metadataBytes := loadChartMetadataForSigning(t, testChartfile) archiveData, err := os.ReadFile(testChartfile) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) sig, err := signer.ClearSign(archiveData, filepath.Base(testChartfile), metadataBytes) - if err != nil { - t.Fatalf("failed to sign chart: %v", err) - } + require.NoError(t, err, "failed to sign chart") verification, err := signer.Verify(archiveData, []byte(sig), filepath.Base(testChartfile)) - if err != nil { - t.Fatalf("failed to verify chart signature: %v", err) - } + require.NoError(t, err, "failed to verify chart signature") - if verification.SignedBy == nil { - t.Fatal("expected verification to include signer") - } + require.NotNil(t, verification.SignedBy, "expected verification to include signer") + require.NotNil(t, verification.SignedBy.PrimaryKey, "expected verification to include signer primary key") + assert.Equal(t, packet.PubKeyAlgoRSA, verification.SignedBy.PrimaryKey.PubKeyAlgo, "expected verification to report RSA key") - if verification.SignedBy.PrimaryKey == nil { - t.Fatal("expected verification to include signer primary key") - } - - if verification.SignedBy.PrimaryKey.PubKeyAlgo != packet.PubKeyAlgoRSA { - t.Fatalf("expected verification to report RSA key but got %v", verification.SignedBy.PrimaryKey.PubKeyAlgo) - } - - if _, ok := verification.SignedBy.Identities[testKeyName]; !ok { - t.Fatalf("expected verification to be signed by %q", testKeyName) - } + _, ok := verification.SignedBy.Identities[testKeyName] + assert.True(t, ok, "expected verification to be signed by %q", testKeyName) } // failSigner always fails to sign and returns an error From 0d359d44ea777e0034ed154e057857b4bbf3a4d2 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Thu, 25 Sep 2025 09:13:47 +0200 Subject: [PATCH 053/118] Restore lint rule for excluding meaningless name This rule is needed for packages like 'util'. Changes to those packages trigger this rule. Signed-off-by: Matt Farina --- .golangci.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.golangci.yml b/.golangci.yml index bf19e2838..236dadf7b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -42,6 +42,14 @@ linters: - legacy - std-error-handling + rules: + # This rule is triggered for packages like 'util'. When changes to those packages + # occur it triggers this rule. This exclusion enables making changes to existing + # packages. + - linters: + - revive + text: 'var-naming: avoid meaningless package names' + warn-unused: true settings: From b0f625fb7bf479ab8a7739f70dc6339a9251dada Mon Sep 17 00:00:00 2001 From: Felix Breidenstein Date: Thu, 25 Sep 2025 14:43:59 +0200 Subject: [PATCH 054/118] Fix typo in bug-report issue template Without a newline, the code block won't render Signed-off-by: Felix Breidenstein --- .github/ISSUE_TEMPLATE/bug-report.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yaml b/.github/ISSUE_TEMPLATE/bug-report.yaml index 4309d800b..1637d26a5 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/bug-report.yaml @@ -44,6 +44,7 @@ body: label: Helm version value: |
+ ```console $ helm version # paste output here From c6c157a798cc242800a7ce778ebb3d8ecb9ff77c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:14:31 +0000 Subject: [PATCH 055/118] chore(deps): bump github.com/cyphar/filepath-securejoin Bumps [github.com/cyphar/filepath-securejoin](https://github.com/cyphar/filepath-securejoin) from 0.4.1 to 0.5.0. - [Release notes](https://github.com/cyphar/filepath-securejoin/releases) - [Changelog](https://github.com/cyphar/filepath-securejoin/blob/main/CHANGELOG.md) - [Commits](https://github.com/cyphar/filepath-securejoin/compare/v0.4.1...v0.5.0) --- updated-dependencies: - dependency-name: github.com/cyphar/filepath-securejoin dependency-version: 0.5.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 c08dabc3b..c518b6764 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/Masterminds/vcs v1.13.3 github.com/ProtonMail/go-crypto v1.3.0 github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 - github.com/cyphar/filepath-securejoin v0.4.1 + github.com/cyphar/filepath-securejoin v0.5.0 github.com/distribution/distribution/v3 v3.0.0 github.com/evanphx/json-patch/v5 v5.9.11 github.com/extism/go-sdk v1.7.1 diff --git a/go.sum b/go.sum index 55a63cc8c..ffc8f7453 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= -github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw= +github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= From 472f8b2628770ec8c3cdb41f590e9937f5c7dede Mon Sep 17 00:00:00 2001 From: Rachel Weber Date: Thu, 25 Sep 2025 18:09:12 -0700 Subject: [PATCH 056/118] Fixing rollback and uninstall client WaitStrategy. Signed-off-by: Rachel Weber --- pkg/action/install.go | 1 + pkg/action/rollback.go | 2 +- pkg/action/upgrade.go | 4 +--- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/action/install.go b/pkg/action/install.go index c6d4f723c..4321877b5 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -566,6 +566,7 @@ func (i *Install) failRelease(rel *release.Release, err error) (*release.Release uninstall.DisableHooks = i.DisableHooks uninstall.KeepHistory = false uninstall.Timeout = i.Timeout + uninstall.WaitStrategy = i.WaitStrategy if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil { return rel, fmt.Errorf("an error occurred while uninstalling the release. original install error: %w: %w", err, uninstallErr) } diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index f56052988..7e32e6f44 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -235,7 +235,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas waiter, err := r.cfg.KubeClient.GetWaiter(r.WaitStrategy) if err != nil { - return nil, fmt.Errorf("unable to set metadata visitor from target release: %w", err) + return nil, fmt.Errorf("unable to get waiter: %w", err) } if r.WaitForJobs { if err := waiter.WaitWithJobs(target, r.Timeout); err != nil { diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 3c84570b2..e31f13b9b 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -563,9 +563,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e rollin := NewRollback(u.cfg) rollin.Version = filteredHistory[0].Version - if u.WaitStrategy == kube.HookOnlyStrategy { - rollin.WaitStrategy = kube.StatusWatcherStrategy - } + rollin.WaitStrategy = u.WaitStrategy rollin.WaitForJobs = u.WaitForJobs rollin.DisableHooks = u.DisableHooks rollin.ForceReplace = u.ForceReplace From 535f542b3cd023ff91c1b7920afb4f0982c128ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 21:07:45 +0000 Subject: [PATCH 057/118] chore(deps): bump ossf/scorecard-action from 2.4.2 to 2.4.3 Bumps [ossf/scorecard-action](https://github.com/ossf/scorecard-action) from 2.4.2 to 2.4.3. - [Release notes](https://github.com/ossf/scorecard-action/releases) - [Changelog](https://github.com/ossf/scorecard-action/blob/main/RELEASE.md) - [Commits](https://github.com/ossf/scorecard-action/compare/05b42c624433fc40578a4040d5cf5e36ddca8cde...4eaacf0543bb3f2c246792bd56e8cdeffafb205a) --- updated-dependencies: - dependency-name: ossf/scorecard-action dependency-version: 2.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/scorecards.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 6a44c8afb..7ab07d524 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -33,7 +33,7 @@ jobs: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif From ee322a9671f41dd9e3377b1cc2d57b05358e8757 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 1 Oct 2025 09:04:49 -0600 Subject: [PATCH 058/118] fix: flakey lint test on shuffle Signed-off-by: Terry Howe --- internal/chart/v3/lint/lint_test.go | 17 ++++++++++++----- pkg/chart/v2/lint/lint_test.go | 17 ++++++++++++----- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/internal/chart/v3/lint/lint_test.go b/internal/chart/v3/lint/lint_test.go index 6f5912ae7..d61a9a740 100644 --- a/internal/chart/v3/lint/lint_test.go +++ b/internal/chart/v3/lint/lint_test.go @@ -27,8 +27,6 @@ import ( chartutil "helm.sh/helm/v4/internal/chart/v3/util" ) -var values map[string]interface{} - const namespace = "testNamespace" const badChartDir = "rules/testdata/badchartfile" @@ -41,6 +39,7 @@ const malformedTemplate = "rules/testdata/malformed-template" const invalidChartFileDir = "rules/testdata/invalidchartfile" func TestBadChartV3(t *testing.T) { + var values map[string]any m := RunAll(badChartDir, values, namespace).Messages if len(m) != 8 { t.Errorf("Number of errors %v", len(m)) @@ -90,6 +89,7 @@ func TestBadChartV3(t *testing.T) { } func TestInvalidYaml(t *testing.T) { + var values map[string]any m := RunAll(badYamlFileDir, values, namespace).Messages if len(m) != 1 { t.Fatalf("All didn't fail with expected errors, got %#v", m) @@ -100,6 +100,7 @@ func TestInvalidYaml(t *testing.T) { } func TestInvalidChartYamlV3(t *testing.T) { + var values map[string]any m := RunAll(invalidChartFileDir, values, namespace).Messages t.Log(m) if len(m) != 3 { @@ -111,6 +112,7 @@ func TestInvalidChartYamlV3(t *testing.T) { } func TestBadValuesV3(t *testing.T) { + var values map[string]any m := RunAll(badValuesFileDir, values, namespace).Messages if len(m) < 1 { t.Fatalf("All didn't fail with expected errors, got %#v", m) @@ -121,6 +123,7 @@ func TestBadValuesV3(t *testing.T) { } func TestBadCrdFileV3(t *testing.T) { + var values map[string]any m := RunAll(badCrdFileDir, values, namespace).Messages assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") @@ -128,6 +131,7 @@ func TestBadCrdFileV3(t *testing.T) { } func TestGoodChart(t *testing.T) { + var values map[string]any m := RunAll(goodChartDir, values, namespace).Messages if len(m) != 0 { t.Error("All returned linter messages when it shouldn't have") @@ -141,6 +145,7 @@ func TestGoodChart(t *testing.T) { // // See https://github.com/helm/helm/issues/7923 func TestHelmCreateChart(t *testing.T) { + var values map[string]any dir := t.TempDir() createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) @@ -190,11 +195,11 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { // Add values to enable hpa, and ingress which are disabled by default. // This is the equivalent of: // helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true' - updatedValues := map[string]interface{}{ - "autoscaling": map[string]interface{}{ + updatedValues := map[string]any{ + "autoscaling": map[string]any{ "enabled": true, }, - "ingress": map[string]interface{}{ + "ingress": map[string]any{ "enabled": true, }, } @@ -213,6 +218,7 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { // lint ignores import-values // See https://github.com/helm/helm/issues/9658 func TestSubChartValuesChart(t *testing.T) { + var values map[string]any m := RunAll(subChartValuesDir, values, namespace).Messages if len(m) != 0 { t.Error("All returned linter messages when it shouldn't have") @@ -225,6 +231,7 @@ func TestSubChartValuesChart(t *testing.T) { // lint stuck with malformed template object // See https://github.com/helm/helm/issues/11391 func TestMalformedTemplate(t *testing.T) { + var values map[string]any c := time.After(3 * time.Second) ch := make(chan int, 1) var m []support.Message diff --git a/pkg/chart/v2/lint/lint_test.go b/pkg/chart/v2/lint/lint_test.go index bd3ec1f1f..6f8f137f4 100644 --- a/pkg/chart/v2/lint/lint_test.go +++ b/pkg/chart/v2/lint/lint_test.go @@ -27,8 +27,6 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" ) -var values map[string]interface{} - const namespace = "testNamespace" const badChartDir = "rules/testdata/badchartfile" @@ -41,6 +39,7 @@ const malformedTemplate = "rules/testdata/malformed-template" const invalidChartFileDir = "rules/testdata/invalidchartfile" func TestBadChart(t *testing.T) { + var values map[string]any m := RunAll(badChartDir, values, namespace).Messages if len(m) != 9 { t.Errorf("Number of errors %v", len(m)) @@ -95,6 +94,7 @@ func TestBadChart(t *testing.T) { } func TestInvalidYaml(t *testing.T) { + var values map[string]any m := RunAll(badYamlFileDir, values, namespace).Messages if len(m) != 1 { t.Fatalf("All didn't fail with expected errors, got %#v", m) @@ -105,6 +105,7 @@ func TestInvalidYaml(t *testing.T) { } func TestInvalidChartYaml(t *testing.T) { + var values map[string]any m := RunAll(invalidChartFileDir, values, namespace).Messages if len(m) != 2 { t.Fatalf("All didn't fail with expected errors, got %#v", m) @@ -115,6 +116,7 @@ func TestInvalidChartYaml(t *testing.T) { } func TestBadValues(t *testing.T) { + var values map[string]any m := RunAll(badValuesFileDir, values, namespace).Messages if len(m) < 1 { t.Fatalf("All didn't fail with expected errors, got %#v", m) @@ -125,6 +127,7 @@ func TestBadValues(t *testing.T) { } func TestBadCrdFile(t *testing.T) { + var values map[string]any m := RunAll(badCrdFileDir, values, namespace).Messages assert.Lenf(t, m, 2, "All didn't fail with expected errors, got %#v", m) assert.ErrorContains(t, m[0].Err, "apiVersion is not in 'apiextensions.k8s.io'") @@ -132,6 +135,7 @@ func TestBadCrdFile(t *testing.T) { } func TestGoodChart(t *testing.T) { + var values map[string]any m := RunAll(goodChartDir, values, namespace).Messages if len(m) != 0 { t.Error("All returned linter messages when it shouldn't have") @@ -145,6 +149,7 @@ func TestGoodChart(t *testing.T) { // // See https://github.com/helm/helm/issues/7923 func TestHelmCreateChart(t *testing.T) { + var values map[string]any dir := t.TempDir() createdChart, err := chartutil.Create("testhelmcreatepasseslint", dir) @@ -194,11 +199,11 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { // Add values to enable hpa, and ingress which are disabled by default. // This is the equivalent of: // helm lint checkdeprecatedwarnings --set 'autoscaling.enabled=true,ingress.enabled=true' - updatedValues := map[string]interface{}{ - "autoscaling": map[string]interface{}{ + updatedValues := map[string]any{ + "autoscaling": map[string]any{ "enabled": true, }, - "ingress": map[string]interface{}{ + "ingress": map[string]any{ "enabled": true, }, } @@ -217,6 +222,7 @@ func TestHelmCreateChart_CheckDeprecatedWarnings(t *testing.T) { // lint ignores import-values // See https://github.com/helm/helm/issues/9658 func TestSubChartValuesChart(t *testing.T) { + var values map[string]any m := RunAll(subChartValuesDir, values, namespace).Messages if len(m) != 0 { t.Error("All returned linter messages when it shouldn't have") @@ -229,6 +235,7 @@ func TestSubChartValuesChart(t *testing.T) { // lint stuck with malformed template object // See https://github.com/helm/helm/issues/11391 func TestMalformedTemplate(t *testing.T) { + var values map[string]any c := time.After(3 * time.Second) ch := make(chan int, 1) var m []support.Message From b97452d0e75f9f315e92b90603cadae4a48a030c Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Sun, 21 Sep 2025 08:18:27 -0700 Subject: [PATCH 059/118] feat: `helm version` print Kubernetes (client-go) version Signed-off-by: George Jenkins --- Makefile | 2 ++ internal/version/version.go | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e3e6cb538..f7b6a31b1 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,8 @@ LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_M LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) +LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMajor=$(K8S_MODULES_MAJOR_VER) +LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMinor=$(K8S_MODULES_MINOR_VER) .PHONY: all all: build diff --git a/internal/version/version.go b/internal/version/version.go index aa64e618f..b7f2436a1 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -18,6 +18,7 @@ package version // import "helm.sh/helm/v4/internal/version" import ( "flag" + "fmt" "runtime" "strings" ) @@ -37,6 +38,11 @@ var ( gitCommit = "" // gitTreeState is the state of the git tree gitTreeState = "" + + // The Kubernetes version can be set by LDFLAGS. In order to do that the value + // must be a string. + kubeClientVersionMajor = "" + kubeClientVersionMinor = "" ) // BuildInfo describes the compile time information. @@ -49,6 +55,8 @@ type BuildInfo struct { GitTreeState string `json:"git_tree_state,omitempty"` // GoVersion is the version of the Go compiler used. GoVersion string `json:"go_version,omitempty"` + // KubeClientVersion is the version of client-go Helm was build with + KubeClientVersion string `json:"kube_client_version"` } // GetVersion returns the semver string of the version @@ -67,10 +75,11 @@ func GetUserAgent() string { // Get returns build info func Get() BuildInfo { v := BuildInfo{ - Version: GetVersion(), - GitCommit: gitCommit, - GitTreeState: gitTreeState, - GoVersion: runtime.Version(), + Version: GetVersion(), + GitCommit: gitCommit, + GitTreeState: gitTreeState, + GoVersion: runtime.Version(), + KubeClientVersion: fmt.Sprintf("v%s.%s", kubeClientVersionMajor, kubeClientVersionMinor), } // HACK(bacongobbler): strip out GoVersion during a test run for consistent test output From 7761d937caa5d83a8e59cc42951f85454985e18d Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Wed, 1 Oct 2025 15:32:29 -0700 Subject: [PATCH 060/118] testfix Signed-off-by: George Jenkins --- pkg/cmd/testdata/output/version.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/testdata/output/version.txt b/pkg/cmd/testdata/output/version.txt index 3b138ae77..2d50053f2 100644 --- a/pkg/cmd/testdata/output/version.txt +++ b/pkg/cmd/testdata/output/version.txt @@ -1 +1 @@ -version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:""} +version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v."} From a24318c7f12410e760f2607bf2b12c046b57bb3b Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Thu, 2 Oct 2025 12:50:37 -0600 Subject: [PATCH 061/118] fix: flake upgrade test Signed-off-by: Terry Howe --- pkg/action/upgrade_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 0a436534f..80e3070e5 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -60,9 +60,9 @@ func TestUpgradeRelease_Success(t *testing.T) { ctx, done := context.WithCancel(t.Context()) res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) - done() req.NoError(err) is.Equal(res.Info.Status, release.StatusDeployed) + done() // Detecting previous bug where context termination after successful release // caused release to fail. From 3c2c9bbac6a1062f60288c274e103ff7ec30d99e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:06:31 +0000 Subject: [PATCH 062/118] chore(deps): bump actions/stale from 10.0.0 to 10.1.0 Bumps [actions/stale](https://github.com/actions/stale) from 10.0.0 to 10.1.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/3a9db7e6a41a89f618792c92c0e97cc736e1b13f...5f858e3efba33a5ca4407a664cc011ad407f2008) --- updated-dependencies: - dependency-name: actions/stale dependency-version: 10.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/stale.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index 965410793..3d72d1e17 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,7 +7,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 + - uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue has been marked as stale because it has been open for 90 days with no activity. This thread will be automatically closed in 30 days if no further activity occurs.' From a85e82af932a7c55a953fb4370d74c7e8b922b0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 21:17:22 +0000 Subject: [PATCH 063/118] chore(deps): bump sigs.k8s.io/controller-runtime from 0.22.1 to 0.22.2 Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.22.1 to 0.22.2. - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.22.1...v0.22.2) --- updated-dependencies: - dependency-name: sigs.k8s.io/controller-runtime dependency-version: 0.22.2 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 c518b6764..30664de3b 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.34.1 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.22.1 + sigs.k8s.io/controller-runtime v0.22.2 sigs.k8s.io/kustomize/kyaml v0.20.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index ffc8f7453..02a5171d8 100644 --- a/go.sum +++ b/go.sum @@ -536,8 +536,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.22.1 h1:Ah1T7I+0A7ize291nJZdS1CabF/lB4E++WizgV24Eqg= -sigs.k8s.io/controller-runtime v0.22.1/go.mod h1:FwiwRjkRPbiN+zp2QRp7wlTCzbUXxZ/D4OzuQUDwBHY= +sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4= +sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= From 1477d3793f778583dda9b6dc8895f7c866a0fa81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Carneiro?= Date: Tue, 7 Oct 2025 17:17:07 +0200 Subject: [PATCH 064/118] Clarify the intent of the resource instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fábio Carneiro --- internal/chart/v3/util/create.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go index 9f742e646..c5e728721 100644 --- a/internal/chart/v3/util/create.go +++ b/internal/chart/v3/util/create.go @@ -218,9 +218,10 @@ httpRoute: # 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 + # For publicly distributed charts, we recommend leaving 'resources' commented out. + # This makes resource allocation a conscious choice for the user and increases the chances + # charts run on a wide range of environments from low-resource clusters like Minikube to those + # with strict resource policies. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. # limits: # cpu: 100m From e69a12dad3fe7186a921353039e0f0fa0beb97b3 Mon Sep 17 00:00:00 2001 From: reddaisyy Date: Wed, 8 Oct 2025 10:23:30 +0800 Subject: [PATCH 065/118] refactor: use reflect.TypeFor Signed-off-by: reddaisyy --- internal/plugin/plugin_type_registry.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/internal/plugin/plugin_type_registry.go b/internal/plugin/plugin_type_registry.go index da6546c47..5138422bd 100644 --- a/internal/plugin/plugin_type_registry.go +++ b/internal/plugin/plugin_type_registry.go @@ -73,27 +73,27 @@ type pluginTypeMeta struct { var pluginTypes = []pluginTypeMeta{ { pluginType: "test/v1", - inputType: reflect.TypeOf(schema.InputMessageTestV1{}), - outputType: reflect.TypeOf(schema.OutputMessageTestV1{}), - configType: reflect.TypeOf(schema.ConfigTestV1{}), + inputType: reflect.TypeFor[schema.InputMessageTestV1](), + outputType: reflect.TypeFor[schema.OutputMessageTestV1](), + configType: reflect.TypeFor[schema.ConfigTestV1](), }, { pluginType: "cli/v1", - inputType: reflect.TypeOf(schema.InputMessageCLIV1{}), - outputType: reflect.TypeOf(schema.OutputMessageCLIV1{}), - configType: reflect.TypeOf(schema.ConfigCLIV1{}), + inputType: reflect.TypeFor[schema.InputMessageCLIV1](), + outputType: reflect.TypeFor[schema.OutputMessageCLIV1](), + configType: reflect.TypeFor[schema.ConfigCLIV1](), }, { pluginType: "getter/v1", - inputType: reflect.TypeOf(schema.InputMessageGetterV1{}), - outputType: reflect.TypeOf(schema.OutputMessageGetterV1{}), - configType: reflect.TypeOf(schema.ConfigGetterV1{}), + inputType: reflect.TypeFor[schema.InputMessageGetterV1](), + outputType: reflect.TypeFor[schema.OutputMessageGetterV1](), + configType: reflect.TypeFor[schema.ConfigGetterV1](), }, { pluginType: "postrenderer/v1", - inputType: reflect.TypeOf(schema.InputMessagePostRendererV1{}), - outputType: reflect.TypeOf(schema.OutputMessagePostRendererV1{}), - configType: reflect.TypeOf(schema.ConfigPostRendererV1{}), + inputType: reflect.TypeFor[schema.InputMessagePostRendererV1](), + outputType: reflect.TypeFor[schema.OutputMessagePostRendererV1](), + configType: reflect.TypeFor[schema.ConfigPostRendererV1](), }, } From 368249a55d7a5ef2cdaed93496b2b94564defc32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Oct 2025 21:15:42 +0000 Subject: [PATCH 066/118] chore(deps): bump golang.org/x/text from 0.29.0 to 0.30.0 Bumps [golang.org/x/text](https://github.com/golang/text) from 0.29.0 to 0.30.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.29.0...v0.30.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-version: 0.30.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 30664de3b..a602b0b14 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,7 @@ require ( go.yaml.in/yaml/v3 v3.0.4 golang.org/x/crypto v0.42.0 golang.org/x/term v0.35.0 - golang.org/x/text v0.29.0 + golang.org/x/text v0.30.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.1 k8s.io/apiextensions-apiserver v0.34.1 @@ -162,13 +162,13 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/net v0.44.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect google.golang.org/grpc v1.72.1 // indirect diff --git a/go.sum b/go.sum index 02a5171d8..f269e6d02 100644 --- a/go.sum +++ b/go.sum @@ -404,8 +404,8 @@ golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91 golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -419,8 +419,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -472,8 +472,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -484,8 +484,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From fd3ef2e000a56a2668c134a16fe152314b6e5dfc Mon Sep 17 00:00:00 2001 From: yzewei Date: Fri, 26 Sep 2025 11:01:46 +0800 Subject: [PATCH 067/118] Add loongarch64 support Signed-off-by: yzewei --- Makefile | 4 ++-- scripts/get | 2 +- scripts/get-helm-3 | 2 +- scripts/release-notes.sh | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index e3e6cb538..eed18c7cd 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ BINDIR := $(CURDIR)/bin INSTALL_PATH ?= /usr/local/bin DIST_DIRS := find * -type d -exec -TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64 -TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum +TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64 +TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum BINNAME ?= helm GOBIN = $(shell go env GOBIN) diff --git a/scripts/get b/scripts/get index 45ae3275b..25fd08e76 100755 --- a/scripts/get +++ b/scripts/get @@ -60,7 +60,7 @@ runAsRoot() { # verifySupported checks that the os/arch combination is supported for # binary builds. verifySupported() { - local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64" + local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-loong64\nlinux-ppc64le\nlinux-s390x\nwindows-amd64\nwindows-arm64" if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then echo "No prebuilt binary for ${OS}-${ARCH}." echo "To build from source, go to https://github.com/helm/helm" diff --git a/scripts/get-helm-3 b/scripts/get-helm-3 index 3aa44daee..e4b12c2ad 100755 --- a/scripts/get-helm-3 +++ b/scripts/get-helm-3 @@ -69,7 +69,7 @@ runAsRoot() { # verifySupported checks that the os/arch combination is supported for # binary builds, as well whether or not necessary tools are present. verifySupported() { - local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64" + local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-loong64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64" if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then echo "No prebuilt binary for ${OS}-${ARCH}." echo "To build from source, go to https://github.com/helm/helm" diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh index cea9bf4dc..48328cb38 100755 --- a/scripts/release-notes.sh +++ b/scripts/release-notes.sh @@ -87,6 +87,7 @@ Download Helm ${RELEASE}. The common platform binaries are here: - [Linux arm](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm.tar.gz.sha256)) - [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256)) - [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256)) +- [Linux loong64](https://get.helm.sh/helm-${RELEASE}-linux-loong64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-loong64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-loong64.tar.gz.sha256)) - [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256)) - [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-s390x.tar.gz.sha256)) - [Linux riscv64](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-riscv64.tar.gz.sha256)) From b861de5696899442dfb4b9c62f44943e590307cf Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Thu, 9 Oct 2025 10:24:49 -0700 Subject: [PATCH 068/118] Update pkg/kube/interface.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: George Jenkins --- pkg/kube/interface.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index 89bf08163..246c9757f 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -38,7 +38,8 @@ type Interface interface { // Create creates one or more resources. Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) - // Delete destroys one or more resources. + // Delete destroys one or more resources using the specified deletion propagation policy. + // The 'policy' parameter determines how child resources are handled during deletion. Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) // Update updates one or more resources or creates the resource From 1581eaa034ce6262a8d11532128a8fc7a3c2d323 Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Thu, 9 Oct 2025 10:26:24 -0700 Subject: [PATCH 069/118] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: George Jenkins --- pkg/kube/interface.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/kube/interface.go b/pkg/kube/interface.go index 246c9757f..cc934ae1e 100644 --- a/pkg/kube/interface.go +++ b/pkg/kube/interface.go @@ -59,10 +59,10 @@ type Interface interface { // Get Waiter gets the Kube.Waiter GetWaiter(ws WaitStrategy) (Waiter, error) - // GetPodList list all pods that match the specified listOptions + // GetPodList lists all pods that match the specified listOptions GetPodList(namespace string, listOptions metav1.ListOptions) (*v1.PodList, error) - // OutputContainerLogsForPodList output the logs for a pod list + // OutputContainerLogsForPodList outputs the logs for a pod list OutputContainerLogsForPodList(podList *v1.PodList, namespace string, writerFunc func(namespace, pod, container string) io.Writer) error // BuildTable creates a resource list from a Reader. This differs from From c75026c318d8ee6131e11232bb975a05a1df2a3f Mon Sep 17 00:00:00 2001 From: George Jenkins Date: Thu, 9 Oct 2025 10:27:06 -0700 Subject: [PATCH 070/118] doc string Signed-off-by: George Jenkins --- pkg/kube/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index a1030a316..d0944afdb 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -772,7 +772,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate } // Delete deletes Kubernetes resources specified in the resources list with -// background cascade deletion. It will attempt to delete all resources even +// given deletion propagation policy. It will attempt to delete all resources even // if one or more fail and collect any errors. All successfully deleted items // will be returned in the `Deleted` ResourceList that is part of the result. func (c *Client) Delete(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) { From 4a944230ea8ea78eb157849bbc31f8b80c7cc42a Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Thu, 9 Oct 2025 22:48:42 +0200 Subject: [PATCH 071/118] Do not ignore *.yml file on linting while accepting *.yaml From : https://github.com/helm/helm/pull/30712/files#r2417446037 Signed-off-by: Benoit Tigeot --- internal/chart/v3/lint/rules/template.go | 7 ++++++- pkg/chart/v2/lint/rules/template.go | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/chart/v3/lint/rules/template.go b/internal/chart/v3/lint/rules/template.go index d4c62839f..204966364 100644 --- a/internal/chart/v3/lint/rules/template.go +++ b/internal/chart/v3/lint/rules/template.go @@ -126,7 +126,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) // We only apply the following lint rules to yaml files - if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { + if !isYamlFileExtension(fileName) { continue } @@ -335,6 +335,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error { return nil } +func isYamlFileExtension(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + return ext == ".yaml" || ext == ".yml" +} + // k8sYamlStruct stubs a Kubernetes YAML file. type k8sYamlStruct struct { APIVersion string `json:"apiVersion"` diff --git a/pkg/chart/v2/lint/rules/template.go b/pkg/chart/v2/lint/rules/template.go index 5c84d0f68..c3cded5d3 100644 --- a/pkg/chart/v2/lint/rules/template.go +++ b/pkg/chart/v2/lint/rules/template.go @@ -126,7 +126,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) // We only apply the following lint rules to yaml files - if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { + if !isYamlFileExtension(fileName) { continue } @@ -335,6 +335,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error { return nil } +func isYamlFileExtension(fileName string) bool { + ext := strings.ToLower(filepath.Ext(fileName)) + return ext == ".yaml" || ext == ".yml" +} + // k8sYamlStruct stubs a Kubernetes YAML file. type k8sYamlStruct struct { APIVersion string `json:"apiVersion"` From 235988c906f2f58e05f24af6af89fd2f4e9d1fcd Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 10 Oct 2025 09:33:13 -0600 Subject: [PATCH 072/118] fix: clean up coverage script temp file Signed-off-by: Terry Howe --- scripts/coverage.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 9e85a358e..4a29a68ad 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -18,6 +18,7 @@ set -euo pipefail covermode=${COVERMODE:-atomic} coverdir=$(mktemp -d /tmp/coverage.XXXXXXXXXX) +trap 'rm -rf "${coverdir}"' EXIT profile="${coverdir}/cover.out" html=false target="./..." # by default the whole repository is tested @@ -47,7 +48,7 @@ generate_cover_data() { generate_cover_data go tool cover -func "${profile}" -if [ "${html}" == "true" ] ; then +if [ "${html}" = "true" ] ; then go tool cover -html "${profile}" fi From 94bfe9ba123f19ece200ed6cdcbff77c3799e828 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Thu, 25 Sep 2025 11:02:06 +0200 Subject: [PATCH 073/118] Move action get metadata to chart interfaces Signed-off-by: Matt Farina --- pkg/action/get_metadata.go | 32 ++++++++++----- pkg/action/get_metadata_test.go | 71 +++++++++++++++++++++++---------- pkg/chart/dependency.go | 8 ++++ pkg/chart/interfaces.go | 1 + 4 files changed, 82 insertions(+), 30 deletions(-) diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go index 889545ddc..2677a57ad 100644 --- a/pkg/action/get_metadata.go +++ b/pkg/action/get_metadata.go @@ -17,11 +17,12 @@ limitations under the License. package action import ( + "log/slog" "sort" "strings" "time" - chart "helm.sh/helm/v4/pkg/chart/v2" + ci "helm.sh/helm/v4/pkg/chart" ) // GetMetadata is the action for checking a given release's metadata. @@ -41,13 +42,13 @@ type Metadata struct { // Annotations are fetched from the Chart.yaml file Annotations map[string]string `json:"annotations,omitempty" yaml:"annotations,omitempty"` // Labels of the release which are stored in driver metadata fields storage - Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` - Dependencies []*chart.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` - Namespace string `json:"namespace" yaml:"namespace"` - Revision int `json:"revision" yaml:"revision"` - Status string `json:"status" yaml:"status"` - DeployedAt string `json:"deployedAt" yaml:"deployedAt"` - ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + Dependencies []ci.Dependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` + Namespace string `json:"namespace" yaml:"namespace"` + Revision int `json:"revision" yaml:"revision"` + Status string `json:"status" yaml:"status"` + DeployedAt string `json:"deployedAt" yaml:"deployedAt"` + ApplyMethod string `json:"applyMethod,omitempty" yaml:"applyMethod,omitempty"` } // NewGetMetadata creates a new GetMetadata object with the given configuration. @@ -68,12 +69,17 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) { return nil, err } + ac, err := ci.NewAccessor(rel.Chart) + if err != nil { + return nil, err + } + return &Metadata{ Name: rel.Name, Chart: rel.Chart.Metadata.Name, Version: rel.Chart.Metadata.Version, AppVersion: rel.Chart.Metadata.AppVersion, - Dependencies: rel.Chart.Metadata.Dependencies, + Dependencies: ac.MetaDependencies(), Annotations: rel.Chart.Metadata.Annotations, Labels: rel.Labels, Namespace: rel.Namespace, @@ -88,7 +94,13 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) { func (m *Metadata) FormattedDepNames() string { depsNames := make([]string, 0, len(m.Dependencies)) for _, dep := range m.Dependencies { - depsNames = append(depsNames, dep.Name) + ac, err := ci.NewDependencyAccessor(dep) + if err != nil { + slog.Error("unable to access dependency metadata", "error", err) + continue + } + depsNames = append(depsNames, ac.Name()) + } sort.StringSlice(depsNames).Sort() diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index 7962a2133..27e9f3777 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -25,6 +25,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + ci "helm.sh/helm/v4/pkg/chart" chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" release "helm.sh/helm/v4/pkg/release/v1" @@ -123,13 +124,18 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) { result, err := client.Run(releaseName) require.NoError(t, err) + dep0, err := ci.NewDependencyAccessor(result.Dependencies[0]) + require.NoError(t, err) + dep1, err := ci.NewDependencyAccessor(result.Dependencies[1]) + require.NoError(t, err) + assert.Equal(t, releaseName, result.Name) assert.Equal(t, "test-chart", result.Chart) assert.Equal(t, "1.0.0", result.Version) - assert.Equal(t, dependencies, result.Dependencies) + assert.Equal(t, convertDeps(dependencies), result.Dependencies) assert.Len(t, result.Dependencies, 2) - assert.Equal(t, "mysql", result.Dependencies[0].Name) - assert.Equal(t, "redis", result.Dependencies[1].Name) + assert.Equal(t, "mysql", dep0.Name()) + assert.Equal(t, "redis", dep1.Name()) } func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { @@ -177,15 +183,20 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { result, err := client.Run(releaseName) require.NoError(t, err) + dep0, err := ci.NewDependencyAccessor(result.Dependencies[0]) + require.NoError(t, err) + dep1, err := ci.NewDependencyAccessor(result.Dependencies[1]) + require.NoError(t, err) + assert.Equal(t, releaseName, result.Name) assert.Equal(t, "test-chart", result.Chart) assert.Equal(t, "1.0.0", result.Version) - assert.Equal(t, dependencies, result.Dependencies) + assert.Equal(t, convertDeps(dependencies), result.Dependencies) assert.Len(t, result.Dependencies, 2) - assert.Equal(t, "mysql", result.Dependencies[0].Name) - assert.Equal(t, "database", result.Dependencies[0].Alias) - assert.Equal(t, "redis", result.Dependencies[1].Name) - assert.Equal(t, "cache", result.Dependencies[1].Alias) + assert.Equal(t, "mysql", dep0.Name()) + assert.Equal(t, "database", dep0.Alias()) + assert.Equal(t, "redis", dep1.Name()) + assert.Equal(t, "cache", dep1.Alias()) } func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { @@ -243,23 +254,32 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { result, err := client.Run(releaseName) require.NoError(t, err) + dep0, err := ci.NewDependencyAccessor(result.Dependencies[0]) + require.NoError(t, err) + dep1, err := ci.NewDependencyAccessor(result.Dependencies[1]) + require.NoError(t, err) + dep2, err := ci.NewDependencyAccessor(result.Dependencies[2]) + require.NoError(t, err) + dep3, err := ci.NewDependencyAccessor(result.Dependencies[3]) + require.NoError(t, err) + assert.Equal(t, releaseName, result.Name) assert.Equal(t, "test-chart", result.Chart) assert.Equal(t, "1.0.0", result.Version) - assert.Equal(t, dependencies, result.Dependencies) + assert.Equal(t, convertDeps(dependencies), result.Dependencies) assert.Len(t, result.Dependencies, 4) // Verify dependencies with aliases - assert.Equal(t, "mysql", result.Dependencies[0].Name) - assert.Equal(t, "database", result.Dependencies[0].Alias) - assert.Equal(t, "redis", result.Dependencies[2].Name) - assert.Equal(t, "cache", result.Dependencies[2].Alias) + assert.Equal(t, "mysql", dep0.Name()) + assert.Equal(t, "database", dep0.Alias()) + assert.Equal(t, "redis", dep2.Name()) + assert.Equal(t, "cache", dep2.Alias()) // Verify dependencies without aliases - assert.Equal(t, "nginx", result.Dependencies[1].Name) - assert.Equal(t, "", result.Dependencies[1].Alias) - assert.Equal(t, "postgresql", result.Dependencies[3].Name) - assert.Equal(t, "", result.Dependencies[3].Alias) + assert.Equal(t, "nginx", dep1.Name()) + assert.Equal(t, "", dep1.Alias()) + assert.Equal(t, "postgresql", dep3.Name()) + assert.Equal(t, "", dep3.Alias()) } func TestGetMetadata_Run_WithAnnotations(t *testing.T) { @@ -515,8 +535,9 @@ func TestMetadata_FormattedDepNames(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + deps := convertDeps(tc.dependencies) metadata := &Metadata{ - Dependencies: tc.dependencies, + Dependencies: deps, } result := metadata.FormattedDepNames() @@ -525,6 +546,14 @@ func TestMetadata_FormattedDepNames(t *testing.T) { } } +func convertDeps(deps []*chart.Dependency) []ci.Dependency { + var newDeps = make([]ci.Dependency, len(deps)) + for i, c := range deps { + newDeps[i] = c + } + return newDeps +} + func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) { dependencies := []*chart.Dependency{ { @@ -546,8 +575,9 @@ func TestMetadata_FormattedDepNames_WithComplexDependencies(t *testing.T) { }, } + deps := convertDeps(dependencies) metadata := &Metadata{ - Dependencies: dependencies, + Dependencies: deps, } result := metadata.FormattedDepNames() @@ -597,8 +627,9 @@ func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + deps := convertDeps(tc.dependencies) metadata := &Metadata{ - Dependencies: tc.dependencies, + Dependencies: deps, } result := metadata.FormattedDepNames() diff --git a/pkg/chart/dependency.go b/pkg/chart/dependency.go index 9f7c90364..864fe6d2c 100644 --- a/pkg/chart/dependency.go +++ b/pkg/chart/dependency.go @@ -47,6 +47,10 @@ func (r *v2DependencyAccessor) Name() string { return r.dep.Name } +func (r *v2DependencyAccessor) Alias() string { + return r.dep.Alias +} + type v3DependencyAccessor struct { dep *v3chart.Dependency } @@ -54,3 +58,7 @@ type v3DependencyAccessor struct { func (r *v3DependencyAccessor) Name() string { return r.dep.Name } + +func (r *v3DependencyAccessor) Alias() string { + return r.dep.Alias +} diff --git a/pkg/chart/interfaces.go b/pkg/chart/interfaces.go index f9c61c35c..4001bc548 100644 --- a/pkg/chart/interfaces.go +++ b/pkg/chart/interfaces.go @@ -40,4 +40,5 @@ type Accessor interface { type DependencyAccessor interface { Name() string + Alias() string } From 2247a0074ba4062be63d30f8bbcf1fae4fc8fae3 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Thu, 25 Sep 2025 12:15:53 +0200 Subject: [PATCH 074/118] Move all the get commands to the new release interface Signed-off-by: Matt Farina --- pkg/action/get.go | 4 +- pkg/cmd/get_hooks.go | 13 ++++++- pkg/cmd/get_manifest.go | 7 +++- pkg/cmd/get_notes.go | 9 ++++- pkg/cmd/status.go | 71 ++++++++++++++++++++-------------- pkg/release/common.go | 81 +++++++++++++++++++++++++++++++++++++++ pkg/release/interfaces.go | 32 ++++++++++++++++ 7 files changed, 181 insertions(+), 36 deletions(-) create mode 100644 pkg/release/common.go create mode 100644 pkg/release/interfaces.go diff --git a/pkg/action/get.go b/pkg/action/get.go index dbe5f4cb3..b5e7c194b 100644 --- a/pkg/action/get.go +++ b/pkg/action/get.go @@ -17,7 +17,7 @@ limitations under the License. package action import ( - release "helm.sh/helm/v4/pkg/release/v1" + release "helm.sh/helm/v4/pkg/release" ) // Get is the action for checking a given release's information. @@ -38,7 +38,7 @@ func NewGet(cfg *Configuration) *Get { } // Run executes 'helm get' against the given release. -func (g *Get) Run(name string) (*release.Release, error) { +func (g *Get) Run(name string) (release.Releaser, error) { if err := g.cfg.KubeClient.IsReachable(); err != nil { return nil, err } diff --git a/pkg/cmd/get_hooks.go b/pkg/cmd/get_hooks.go index 7ffefd93c..d344307cb 100644 --- a/pkg/cmd/get_hooks.go +++ b/pkg/cmd/get_hooks.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/cmd/require" + "helm.sh/helm/v4/pkg/release" ) const getHooksHelp = ` @@ -52,8 +53,16 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - for _, hook := range res.Hooks { - fmt.Fprintf(out, "---\n# Source: %s\n%s\n", hook.Path, hook.Manifest) + rac, err := release.NewAccessor(res) + if err != nil { + return err + } + for _, hook := range rac.Hooks() { + hac, err := release.NewHookAccessor(hook) + if err != nil { + return err + } + fmt.Fprintf(out, "---\n# Source: %s\n%s\n", hac.Path(), hac.Manifest()) } return nil }, diff --git a/pkg/cmd/get_manifest.go b/pkg/cmd/get_manifest.go index 021495d8d..253b011c1 100644 --- a/pkg/cmd/get_manifest.go +++ b/pkg/cmd/get_manifest.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/cmd/require" + "helm.sh/helm/v4/pkg/release" ) var getManifestHelp = ` @@ -54,7 +55,11 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command if err != nil { return err } - fmt.Fprintln(out, res.Manifest) + rac, err := release.NewAccessor(res) + if err != nil { + return err + } + fmt.Fprintln(out, rac.Manifest()) return nil }, } diff --git a/pkg/cmd/get_notes.go b/pkg/cmd/get_notes.go index ae79d8bcc..46fbeeaf5 100644 --- a/pkg/cmd/get_notes.go +++ b/pkg/cmd/get_notes.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/cmd/require" + "helm.sh/helm/v4/pkg/release" ) var getNotesHelp = ` @@ -50,8 +51,12 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if err != nil { return err } - if len(res.Info.Notes) > 0 { - fmt.Fprintf(out, "NOTES:\n%s\n", res.Info.Notes) + rac, err := release.NewAccessor(res) + if err != nil { + return err + } + if len(rac.Notes()) > 0 { + fmt.Fprintf(out, "NOTES:\n%s\n", rac.Notes()) } return nil }, diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 3d1309c3e..1988dc5c7 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -33,7 +33,8 @@ import ( "helm.sh/helm/v4/pkg/chart/common/util" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cmd/require" - release "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/release" + releasev1 "helm.sh/helm/v4/pkg/release/v1" ) // NOTE: Keep the list of statuses up-to-date with pkg/release/status.go. @@ -110,54 +111,66 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } type statusPrinter struct { - release *release.Release + release release.Releaser debug bool showMetadata bool hideNotes bool noColor bool } +func (s statusPrinter) getV1Release() *releasev1.Release { + switch rel := s.release.(type) { + case releasev1.Release: + return &rel + case *releasev1.Release: + return rel + } + return &releasev1.Release{} +} + func (s statusPrinter) WriteJSON(out io.Writer) error { - return output.EncodeJSON(out, s.release) + return output.EncodeJSON(out, s.getV1Release()) } func (s statusPrinter) WriteYAML(out io.Writer) error { - return output.EncodeYAML(out, s.release) + return output.EncodeYAML(out, s.getV1Release()) } func (s statusPrinter) WriteTable(out io.Writer) error { if s.release == nil { return nil } - _, _ = fmt.Fprintf(out, "NAME: %s\n", s.release.Name) - if !s.release.Info.LastDeployed.IsZero() { - _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", s.release.Info.LastDeployed.Format(time.ANSIC)) + rel := s.getV1Release() + fmt.Printf("%+v", rel) + _, _ = fmt.Fprintf(out, "NAME: %s\n", rel.Name) + if !rel.Info.LastDeployed.IsZero() { + _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Format(time.ANSIC)) } - _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(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) + _, _ = fmt.Fprintf(out, "NAMESPACE: %s\n", coloroutput.ColorizeNamespace(rel.Namespace, s.noColor)) + _, _ = fmt.Fprintf(out, "STATUS: %s\n", coloroutput.ColorizeStatus(rel.Info.Status, s.noColor)) + _, _ = fmt.Fprintf(out, "REVISION: %d\n", rel.Version) if s.showMetadata { - _, _ = fmt.Fprintf(out, "CHART: %s\n", s.release.Chart.Metadata.Name) - _, _ = fmt.Fprintf(out, "VERSION: %s\n", s.release.Chart.Metadata.Version) - _, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", s.release.Chart.Metadata.AppVersion) + _, _ = fmt.Fprintf(out, "CHART: %s\n", rel.Chart.Metadata.Name) + _, _ = fmt.Fprintf(out, "VERSION: %s\n", rel.Chart.Metadata.Version) + _, _ = fmt.Fprintf(out, "APP_VERSION: %s\n", rel.Chart.Metadata.AppVersion) } - _, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", s.release.Info.Description) + _, _ = fmt.Fprintf(out, "DESCRIPTION: %s\n", rel.Info.Description) - if len(s.release.Info.Resources) > 0 { + if len(rel.Info.Resources) > 0 { buf := new(bytes.Buffer) printFlags := get.NewHumanPrintFlags() typePrinter, _ := printFlags.ToPrinter("") printer := &get.TablePrinter{Delegate: typePrinter} var keys []string - for key := range s.release.Info.Resources { + for key := range rel.Info.Resources { keys = append(keys, key) } for _, t := range keys { _, _ = fmt.Fprintf(buf, "==> %s\n", t) - vk := s.release.Info.Resources[t] + vk := rel.Info.Resources[t] for _, resource := range vk { if err := printer.PrintObj(resource, buf); err != nil { _, _ = fmt.Fprintf(buf, "failed to print object type %s: %v\n", t, err) @@ -170,8 +183,8 @@ func (s statusPrinter) WriteTable(out io.Writer) error { _, _ = fmt.Fprintf(out, "RESOURCES:\n%s\n", buf.String()) } - executions := executionsByHookEvent(s.release) - if tests, ok := executions[release.HookTest]; !ok || len(tests) == 0 { + executions := executionsByHookEvent(rel) + if tests, ok := executions[releasev1.HookTest]; !ok || len(tests) == 0 { _, _ = fmt.Fprintln(out, "TEST SUITE: None") } else { for _, h := range tests { @@ -190,14 +203,14 @@ func (s statusPrinter) WriteTable(out io.Writer) error { if s.debug { _, _ = fmt.Fprintln(out, "USER-SUPPLIED VALUES:") - err := output.EncodeYAML(out, s.release.Config) + err := output.EncodeYAML(out, rel.Config) if err != nil { return err } // Print an extra newline _, _ = fmt.Fprintln(out) - cfg, err := util.CoalesceValues(s.release.Chart, s.release.Config) + cfg, err := util.CoalesceValues(rel.Chart, rel.Config) if err != nil { return err } @@ -211,28 +224,28 @@ func (s statusPrinter) WriteTable(out io.Writer) error { _, _ = fmt.Fprintln(out) } - if strings.EqualFold(s.release.Info.Description, "Dry run complete") || s.debug { + if strings.EqualFold(rel.Info.Description, "Dry run complete") || s.debug { _, _ = fmt.Fprintln(out, "HOOKS:") - for _, h := range s.release.Hooks { + for _, h := range rel.Hooks { _, _ = fmt.Fprintf(out, "---\n# Source: %s\n%s\n", h.Path, h.Manifest) } - _, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", s.release.Manifest) + _, _ = fmt.Fprintf(out, "MANIFEST:\n%s\n", rel.Manifest) } // 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)) + if !s.hideNotes && len(rel.Info.Notes) > 0 { + _, _ = fmt.Fprintf(out, "NOTES:\n%s\n", strings.TrimSpace(rel.Info.Notes)) } return nil } -func executionsByHookEvent(rel *release.Release) map[release.HookEvent][]*release.Hook { - result := make(map[release.HookEvent][]*release.Hook) +func executionsByHookEvent(rel *releasev1.Release) map[releasev1.HookEvent][]*releasev1.Hook { + result := make(map[releasev1.HookEvent][]*releasev1.Hook) for _, h := range rel.Hooks { for _, e := range h.Events { executions, ok := result[e] if !ok { - executions = []*release.Hook{} + executions = []*releasev1.Hook{} } result[e] = append(executions, h) } diff --git a/pkg/release/common.go b/pkg/release/common.go new file mode 100644 index 000000000..5cdcf9b88 --- /dev/null +++ b/pkg/release/common.go @@ -0,0 +1,81 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +import ( + "errors" + + v1release "helm.sh/helm/v4/pkg/release/v1" +) + +var NewAccessor func(rel Releaser) (Accessor, error) = NewDefaultAccessor //nolint:revive + +var NewHookAccessor func(rel Hook) (HookAccessor, error) = NewDefaultHookAccessor //nolint:revive + +func NewDefaultAccessor(rel Releaser) (Accessor, error) { + switch v := rel.(type) { + case v1release.Release: + return &v1Accessor{&v}, nil + case *v1release.Release: + return &v1Accessor{v}, nil + default: + return nil, errors.New("unsupported release type") + } +} + +func NewDefaultHookAccessor(hook Hook) (HookAccessor, error) { + switch h := hook.(type) { + case v1release.Hook: + return &v1HookAccessor{&h}, nil + case *v1release.Hook: + return &v1HookAccessor{h}, nil + default: + return nil, errors.New("unsupported release hook type") + } +} + +type v1Accessor struct { + rel *v1release.Release +} + +func (a *v1Accessor) Hooks() []Hook { + var hooks = make([]Hook, len(a.rel.Hooks)) + for i, h := range a.rel.Hooks { + hooks[i] = h + } + return hooks +} + +func (a *v1Accessor) Manifest() string { + return a.rel.Manifest +} + +func (a *v1Accessor) Notes() string { + return a.rel.Info.Notes +} + +type v1HookAccessor struct { + hook *v1release.Hook +} + +func (a *v1HookAccessor) Path() string { + return a.hook.Path +} + +func (a *v1HookAccessor) Manifest() string { + return a.hook.Manifest +} diff --git a/pkg/release/interfaces.go b/pkg/release/interfaces.go new file mode 100644 index 000000000..9d6987625 --- /dev/null +++ b/pkg/release/interfaces.go @@ -0,0 +1,32 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +type Releaser interface{} + +type Hook interface{} + +type Accessor interface { + Hooks() []Hook + Manifest() string + Notes() string +} + +type HookAccessor interface { + Path() string + Manifest() string +} From f80cbe43d07265ce9d233bb78dfd9404c0936f44 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 8 Oct 2025 17:57:55 -0400 Subject: [PATCH 075/118] Moved release objects to enable versioning Signed-off-by: Matt Farina --- internal/cli/output/color.go | 12 +- internal/cli/output/color_test.go | 18 +-- pkg/action/action.go | 3 +- pkg/action/action_test.go | 5 +- pkg/action/get_metadata.go | 43 +++++-- pkg/action/get_metadata_test.go | 38 +++--- pkg/action/get_values.go | 30 ++++- pkg/action/get_values_test.go | 9 +- pkg/action/history.go | 4 +- pkg/action/hooks_test.go | 5 +- pkg/action/install.go | 60 +++++++-- pkg/action/install_test.go | 55 ++++++--- pkg/action/list.go | 34 ++++-- pkg/action/list_test.go | 66 +++++++--- pkg/action/release_testing.go | 10 +- pkg/action/rollback.go | 40 ++++-- pkg/action/status.go | 7 +- pkg/action/uninstall.go | 23 +++- pkg/action/uninstall_test.go | 4 +- pkg/action/upgrade.go | 36 ++++-- pkg/action/upgrade_test.go | 120 ++++++++++-------- pkg/cmd/completion_test.go | 3 +- pkg/cmd/flags_test.go | 11 +- pkg/cmd/history.go | 12 +- pkg/cmd/history_test.go | 33 ++--- pkg/cmd/list.go | 35 ++++-- pkg/cmd/list_test.go | 21 ++-- pkg/cmd/release_testing.go | 8 +- pkg/cmd/release_testing_test.go | 3 +- pkg/cmd/rollback_test.go | 27 +++-- pkg/cmd/root.go | 29 +++++ pkg/cmd/status_test.go | 23 ++-- pkg/cmd/upgrade.go | 12 +- pkg/cmd/upgrade_test.go | 51 ++++++-- pkg/release/common.go | 37 +++++- pkg/release/{v1 => common}/status.go | 2 +- pkg/release/interfaces.go | 14 +++ pkg/release/v1/info.go | 3 +- pkg/release/v1/mock.go | 5 +- pkg/release/v1/release.go | 3 +- pkg/release/v1/util/filter.go | 7 +- pkg/release/v1/util/filter_test.go | 11 +- pkg/release/v1/util/sorter_test.go | 11 +- pkg/storage/driver/cfgmaps.go | 38 ++++-- pkg/storage/driver/cfgmaps_test.go | 73 +++++++---- pkg/storage/driver/driver.go | 28 ++++- pkg/storage/driver/memory.go | 27 +++-- pkg/storage/driver/memory_test.go | 52 +++++--- pkg/storage/driver/mock_test.go | 27 +++-- pkg/storage/driver/records_test.go | 42 +++---- pkg/storage/driver/secrets.go | 27 +++-- pkg/storage/driver/secrets_test.go | 58 +++++---- pkg/storage/driver/sql.go | 26 ++-- pkg/storage/driver/sql_test.go | 45 ++++--- pkg/storage/storage.go | 136 ++++++++++++++++----- pkg/storage/storage_test.go | 175 +++++++++++++++------------ 56 files changed, 1165 insertions(+), 572 deletions(-) rename pkg/release/{v1 => common}/status.go (99%) diff --git a/internal/cli/output/color.go b/internal/cli/output/color.go index 93bbbe56e..e59cdde87 100644 --- a/internal/cli/output/color.go +++ b/internal/cli/output/color.go @@ -19,24 +19,24 @@ package output import ( "github.com/fatih/color" - release "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/release/common" ) // ColorizeStatus returns a colorized version of the status string based on the status value -func ColorizeStatus(status release.Status, noColor bool) string { +func ColorizeStatus(status common.Status, noColor bool) string { // Disable color if requested if noColor { return status.String() } switch status { - case release.StatusDeployed: + case common.StatusDeployed: return color.GreenString(status.String()) - case release.StatusFailed: + case common.StatusFailed: return color.RedString(status.String()) - case release.StatusPendingInstall, release.StatusPendingUpgrade, release.StatusPendingRollback, release.StatusUninstalling: + case common.StatusPendingInstall, common.StatusPendingUpgrade, common.StatusPendingRollback, common.StatusUninstalling: return color.YellowString(status.String()) - case release.StatusUnknown: + case common.StatusUnknown: return color.RedString(status.String()) default: // For uninstalled, superseded, and any other status diff --git a/internal/cli/output/color_test.go b/internal/cli/output/color_test.go index c84e2c359..3b8de39e8 100644 --- a/internal/cli/output/color_test.go +++ b/internal/cli/output/color_test.go @@ -20,63 +20,63 @@ import ( "strings" "testing" - release "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/release/common" ) func TestColorizeStatus(t *testing.T) { tests := []struct { name string - status release.Status + status common.Status noColor bool envNoColor string wantColor bool // whether we expect color codes in output }{ { name: "deployed status with color", - status: release.StatusDeployed, + status: common.StatusDeployed, noColor: false, envNoColor: "", wantColor: true, }, { name: "deployed status without color flag", - status: release.StatusDeployed, + status: common.StatusDeployed, noColor: true, envNoColor: "", wantColor: false, }, { name: "deployed status with NO_COLOR env", - status: release.StatusDeployed, + status: common.StatusDeployed, noColor: false, envNoColor: "1", wantColor: false, }, { name: "failed status with color", - status: release.StatusFailed, + status: common.StatusFailed, noColor: false, envNoColor: "", wantColor: true, }, { name: "pending install status with color", - status: release.StatusPendingInstall, + status: common.StatusPendingInstall, noColor: false, envNoColor: "", wantColor: true, }, { name: "unknown status with color", - status: release.StatusUnknown, + status: common.StatusUnknown, noColor: false, envNoColor: "", wantColor: true, }, { name: "superseded status with color", - status: release.StatusSuperseded, + status: common.StatusSuperseded, noColor: false, envNoColor: "", wantColor: false, // superseded doesn't get colored diff --git a/pkg/action/action.go b/pkg/action/action.go index a3db047b4..fd75b85d3 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -47,6 +47,7 @@ import ( "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/registry" + ri "helm.sh/helm/v4/pkg/release" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage" @@ -412,7 +413,7 @@ func (cfg *Configuration) Now() time.Time { return Timestamper() } -func (cfg *Configuration) releaseContent(name string, version int) (*release.Release, error) { +func (cfg *Configuration) releaseContent(name string, version int) (ri.Releaser, error) { if err := chartutil.ValidateReleaseName(name); err != nil { return nil, fmt.Errorf("releaseContent: Release name is invalid: %s", name) } diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index d993da100..06329095e 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -36,6 +36,7 @@ import ( "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/registry" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" @@ -249,10 +250,10 @@ func withKube(version string) chartOption { // releaseStub creates a release stub, complete with the chartStub as its chart. func releaseStub() *release.Release { - return namedReleaseStub("angry-panda", release.StatusDeployed) + return namedReleaseStub("angry-panda", rcommon.StatusDeployed) } -func namedReleaseStub(name string, status release.Status) *release.Release { +func namedReleaseStub(name string, status rcommon.Status) *release.Release { now := time.Now() return &release.Release{ Name: name, diff --git a/pkg/action/get_metadata.go b/pkg/action/get_metadata.go index 2677a57ad..5312dac7f 100644 --- a/pkg/action/get_metadata.go +++ b/pkg/action/get_metadata.go @@ -17,12 +17,15 @@ limitations under the License. package action import ( + "errors" "log/slog" "sort" "strings" "time" ci "helm.sh/helm/v4/pkg/chart" + chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release" ) // GetMetadata is the action for checking a given release's metadata. @@ -69,24 +72,40 @@ func (g *GetMetadata) Run(name string) (*Metadata, error) { return nil, err } - ac, err := ci.NewAccessor(rel.Chart) + rac, err := release.NewAccessor(rel) if err != nil { return nil, err } + ac, err := ci.NewAccessor(rac.Chart()) + if err != nil { + return nil, err + } + + charti := rac.Chart() + + var chrt *chart.Chart + switch c := charti.(type) { + case *chart.Chart: + chrt = c + case chart.Chart: + chrt = &c + default: + return nil, errors.New("invalid chart apiVersion") + } return &Metadata{ - Name: rel.Name, - Chart: rel.Chart.Metadata.Name, - Version: rel.Chart.Metadata.Version, - AppVersion: rel.Chart.Metadata.AppVersion, + Name: rac.Name(), + Chart: chrt.Metadata.Name, + Version: chrt.Metadata.Version, + AppVersion: chrt.Metadata.AppVersion, Dependencies: ac.MetaDependencies(), - Annotations: rel.Chart.Metadata.Annotations, - Labels: rel.Labels, - Namespace: rel.Namespace, - Revision: rel.Version, - Status: rel.Info.Status.String(), - DeployedAt: rel.Info.LastDeployed.Format(time.RFC3339), - ApplyMethod: rel.ApplyMethod, + Annotations: chrt.Metadata.Annotations, + Labels: rac.Labels(), + Namespace: rac.Namespace(), + Revision: rac.Version(), + Status: rac.Status(), + DeployedAt: rac.DeployedAt().Format(time.RFC3339), + ApplyMethod: rac.ApplyMethod(), }, nil } diff --git a/pkg/action/get_metadata_test.go b/pkg/action/get_metadata_test.go index 27e9f3777..cd5988d8e 100644 --- a/pkg/action/get_metadata_test.go +++ b/pkg/action/get_metadata_test.go @@ -28,6 +28,7 @@ import ( ci "helm.sh/helm/v4/pkg/chart" chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -50,7 +51,7 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -64,7 +65,8 @@ func TestGetMetadata_Run_BasicMetadata(t *testing.T) { Namespace: "default", } - cfg.Releases.Create(rel) + err := cfg.Releases.Create(rel) + require.NoError(t, err) result, err := client.Run(releaseName) require.NoError(t, err) @@ -104,7 +106,7 @@ func TestGetMetadata_Run_WithDependencies(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -163,7 +165,7 @@ func TestGetMetadata_Run_WithDependenciesAliases(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -234,7 +236,7 @@ func TestGetMetadata_Run_WithMixedDependencies(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -298,7 +300,7 @@ func TestGetMetadata_Run_WithAnnotations(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -337,7 +339,7 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) { rel1 := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusSuperseded, + Status: common.StatusSuperseded, LastDeployed: deployedTime.Add(-time.Hour), }, Chart: &chart.Chart{ @@ -354,7 +356,7 @@ func TestGetMetadata_Run_SpecificVersion(t *testing.T) { rel2 := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -388,16 +390,16 @@ func TestGetMetadata_Run_DifferentStatuses(t *testing.T) { testCases := []struct { name string - status release.Status + status common.Status expected string }{ - {"deployed", release.StatusDeployed, "deployed"}, - {"failed", release.StatusFailed, "failed"}, - {"uninstalled", release.StatusUninstalled, "uninstalled"}, - {"pending-install", release.StatusPendingInstall, "pending-install"}, - {"pending-upgrade", release.StatusPendingUpgrade, "pending-upgrade"}, - {"pending-rollback", release.StatusPendingRollback, "pending-rollback"}, - {"superseded", release.StatusSuperseded, "superseded"}, + {"deployed", common.StatusDeployed, "deployed"}, + {"failed", common.StatusFailed, "failed"}, + {"uninstalled", common.StatusUninstalled, "uninstalled"}, + {"pending-install", common.StatusPendingInstall, "pending-install"}, + {"pending-upgrade", common.StatusPendingUpgrade, "pending-upgrade"}, + {"pending-rollback", common.StatusPendingRollback, "pending-rollback"}, + {"superseded", common.StatusSuperseded, "superseded"}, } for _, tc := range testCases { @@ -464,7 +466,7 @@ func TestGetMetadata_Run_EmptyAppVersion(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, LastDeployed: deployedTime, }, Chart: &chart.Chart{ @@ -640,7 +642,7 @@ func TestMetadata_FormattedDepNames_WithAliases(t *testing.T) { func TestGetMetadata_Labels(t *testing.T) { rel := releaseStub() - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed customLabels := map[string]string{"key1": "value1", "key2": "value2"} rel.Labels = customLabels diff --git a/pkg/action/get_values.go b/pkg/action/get_values.go index a0b5d92c1..6475a140b 100644 --- a/pkg/action/get_values.go +++ b/pkg/action/get_values.go @@ -16,7 +16,13 @@ limitations under the License. package action -import "helm.sh/helm/v4/pkg/chart/common/util" +import ( + "fmt" + + "helm.sh/helm/v4/pkg/chart/common/util" + release "helm.sh/helm/v4/pkg/release" + rspb "helm.sh/helm/v4/pkg/release/v1" +) // GetValues is the action for checking a given release's values. // @@ -41,7 +47,12 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) { return nil, err } - rel, err := g.cfg.releaseContent(name, g.Version) + reli, err := g.cfg.releaseContent(name, g.Version) + if err != nil { + return nil, err + } + + rel, err := releaserToV1Release(reli) if err != nil { return nil, err } @@ -56,3 +67,18 @@ func (g *GetValues) Run(name string) (map[string]interface{}, error) { } return rel.Config, nil } + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { + switch r := rel.(type) { + case rspb.Release: + return &r, nil + case *rspb.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} diff --git a/pkg/action/get_values_test.go b/pkg/action/get_values_test.go index b8630c322..8e6588454 100644 --- a/pkg/action/get_values_test.go +++ b/pkg/action/get_values_test.go @@ -26,6 +26,7 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -58,7 +59,7 @@ func TestGetValues_Run_UserConfigOnly(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -112,7 +113,7 @@ func TestGetValues_Run_AllValues(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -147,7 +148,7 @@ func TestGetValues_Run_EmptyValues(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -198,7 +199,7 @@ func TestGetValues_Run_NilConfig(t *testing.T) { rel := &release.Release{ Name: releaseName, Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ diff --git a/pkg/action/history.go b/pkg/action/history.go index d7af1d6a4..dc3ab51d4 100644 --- a/pkg/action/history.go +++ b/pkg/action/history.go @@ -22,7 +22,7 @@ import ( "fmt" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" - release "helm.sh/helm/v4/pkg/release/v1" + release "helm.sh/helm/v4/pkg/release" ) // History is the action for checking the release's ledger. @@ -46,7 +46,7 @@ func NewHistory(cfg *Configuration) *History { } // Run executes 'helm history' against the given release. -func (h *History) Run(name string) ([]*release.Release, error) { +func (h *History) Run(name string) ([]release.Releaser, error) { if err := h.cfg.KubeClient.IsReachable(); err != nil { return nil, err } diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index fb7d1b4ec..f43c45d77 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -33,6 +33,7 @@ import ( "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" "helm.sh/helm/v4/pkg/storage/driver" @@ -187,7 +188,7 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str res, err := instAction.Run(buildChartWithTemplates(templates), vals) is.NoError(err) is.Equal(expectedOutput, outBuffer.String()) - is.Equal(release.StatusDeployed, res.Info.Status) + is.Equal(rcommon.StatusDeployed, res.Info.Status) } func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace string, shouldOutput bool) { @@ -215,7 +216,7 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str is.Error(err) is.Contains(res.Info.Description, "failed pre-install") is.Equal(expectedOutput, outBuffer.String()) - is.Equal(release.StatusFailed, res.Info.Status) + is.Equal(rcommon.StatusFailed, res.Info.Status) } type HookFailedError struct{} diff --git a/pkg/action/install.go b/pkg/action/install.go index 6c5d62d81..49a36b276 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -53,6 +53,8 @@ import ( kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/registry" + ri "helm.sh/helm/v4/pkg/release" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/repo/v1" @@ -353,13 +355,13 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st } // Check error from render if err != nil { - rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) + rel.SetStatus(rcommon.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) // Return a release with partial data so that the client can show debugging information. return rel, err } // Mark this release as in-progress - rel.SetStatus(release.StatusPendingInstall, "Initial install underway") + rel.SetStatus(rcommon.StatusPendingInstall, "Initial install underway") var toBeAdopted kube.ResourceList resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation) @@ -524,9 +526,9 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource } if len(i.Description) > 0 { - rel.SetStatus(release.StatusDeployed, i.Description) + rel.SetStatus(rcommon.StatusDeployed, i.Description) } else { - rel.SetStatus(release.StatusDeployed, "Install complete") + rel.SetStatus(rcommon.StatusDeployed, "Install complete") } // This is a tricky case. The release has been created, but the result @@ -544,7 +546,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource } func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { - rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) + rel.SetStatus(rcommon.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) if i.RollbackOnFailure { slog.Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName) uninstall := NewUninstall(i.cfg) @@ -583,15 +585,43 @@ func (i *Install) availableName() error { if err != nil || len(h) < 1 { return nil } - releaseutil.Reverse(h, releaseutil.SortByRevision) - rel := h[0] - if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) { + hl, err := releaseListToV1List(h) + if err != nil { + return err + } + + releaseutil.Reverse(hl, releaseutil.SortByRevision) + rel := hl[0] + + if st := rel.Info.Status; i.Replace && (st == rcommon.StatusUninstalled || st == rcommon.StatusFailed) { return nil } return errors.New("cannot reuse a name that is still in use") } +func releaseListToV1List(ls []ri.Releaser) ([]*release.Release, error) { + rls := make([]*release.Release, 0, len(ls)) + for _, val := range ls { + rel, err := releaserToV1Release(val) + if err != nil { + return nil, err + } + rls = append(rls, rel) + } + + return rls, nil +} + +func releaseV1ListToReleaserList(ls []*release.Release) ([]ri.Releaser, error) { + rls := make([]ri.Releaser, 0, len(ls)) + for _, val := range ls { + rls = append(rls, val) + } + + return rls, nil +} + // createRelease creates a new release object func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}, labels map[string]string) *release.Release { ts := i.cfg.Now() @@ -604,7 +634,7 @@ func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{ Info: &release.Info{ FirstDeployed: ts, LastDeployed: ts, - Status: release.StatusUnknown, + Status: rcommon.StatusUnknown, }, Version: 1, Labels: labels, @@ -630,20 +660,24 @@ func (i *Install) replaceRelease(rel *release.Release) error { // No releases exist for this name, so we can return early return nil } + hl, err := releaseListToV1List(hist) + if err != nil { + return err + } - releaseutil.Reverse(hist, releaseutil.SortByRevision) - last := hist[0] + releaseutil.Reverse(hl, releaseutil.SortByRevision) + last := hl[0] // Update version to the next available rel.Version = last.Version + 1 // Do not change the status of a failed release. - if last.Info.Status == release.StatusFailed { + if last.Info.Status == rcommon.StatusFailed { return nil } // For any other status, mark it as superseded and store the old record - last.SetStatus(release.StatusSuperseded, "superseded by new release") + last.SetStatus(rcommon.StatusSuperseded, "superseded by new release") return i.recordRelease(last) } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 8d1fc149d..535b14559 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -47,6 +47,7 @@ import ( "helm.sh/helm/v4/pkg/chart/common" "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage/driver" ) @@ -137,7 +138,10 @@ func TestInstallRelease(t *testing.T) { is.Equal(res.Name, "test-install-release", "Expected release name.") is.Equal(res.Namespace, "spaced") - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + rel, err := releaserToV1Release(r) is.NoError(err) is.Len(rel.Hooks, 1) @@ -156,7 +160,9 @@ func TestInstallRelease(t *testing.T) { time.Sleep(time.Millisecond * 100) lastRelease, err := instAction.cfg.Releases.Last(rel.Name) req.NoError(err) - is.Equal(lastRelease.Info.Status, release.StatusDeployed) + lrel, err := releaserToV1Release(lastRelease) + is.NoError(err) + is.Equal(lrel.Info.Status, rcommon.StatusDeployed) } func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) { @@ -180,7 +186,10 @@ func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) { t.Fatalf("Failed install: %s", err) } - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + rel, err := releaserToV1Release(r) is.NoError(err) is.Equal(rel.Info.Description, "Install complete") @@ -197,7 +206,10 @@ func TestInstallReleaseWithTakeOwnership_ResourceOwned(t *testing.T) { if err != nil { t.Fatalf("Failed install: %s", err) } - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + rel, err := releaserToV1Release(r) is.NoError(err) is.Equal(rel.Info.Description, "Install complete") @@ -234,7 +246,10 @@ func TestInstallReleaseWithValues(t *testing.T) { is.Equal(res.Name, "test-install-release", "Expected release name.") is.Equal(res.Namespace, "spaced") - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + + rel, err := releaserToV1Release(r) is.NoError(err) is.Len(rel.Hooks, 1) @@ -273,7 +288,9 @@ func TestInstallRelease_WithNotes(t *testing.T) { is.Equal(res.Name, "with-notes") is.Equal(res.Namespace, "spaced") - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + rel, err := releaserToV1Release(r) is.NoError(err) is.Len(rel.Hooks, 1) is.Equal(rel.Hooks[0].Manifest, manifestWithHook) @@ -297,7 +314,9 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) { t.Fatalf("Failed install: %s", err) } - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + rel, err := releaserToV1Release(r) is.NoError(err) expectedNotes := fmt.Sprintf("got-%s", res.Name) @@ -316,7 +335,9 @@ func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) { t.Fatalf("Failed install: %s", err) } - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + rel, err := releaserToV1Release(r) is.NoError(err) is.Equal("with-notes", rel.Name) is.Equal("parent", rel.Info.Notes) @@ -335,7 +356,9 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { t.Fatalf("Failed install: %s", err) } - rel, err := instAction.cfg.Releases.Get(res.Name, res.Version) + r, err := instAction.cfg.Releases.Get(res.Name, res.Version) + is.NoError(err) + rel, err := releaserToV1Release(r) is.NoError(err) is.Equal("with-notes", rel.Name) // test run can return as either 'parent\nchild' or 'child\nparent' @@ -478,7 +501,7 @@ func TestInstallRelease_FailedHooks(t *testing.T) { is.Error(err) is.Contains(res.Info.Description, "failed post-install") is.Equal("", outBuffer.String()) - is.Equal(release.StatusFailed, res.Info.Status) + is.Equal(rcommon.StatusFailed, res.Info.Status) } func TestInstallRelease_ReplaceRelease(t *testing.T) { @@ -487,7 +510,7 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) { instAction.Replace = true rel := releaseStub() - rel.Info.Status = release.StatusUninstalled + rel.Info.Status = rcommon.StatusUninstalled instAction.cfg.Releases.Create(rel) instAction.ReleaseName = rel.Name @@ -499,9 +522,11 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) { is.Equal(2, res.Version) is.Equal(res.Name, rel.Name) - getres, err := instAction.cfg.Releases.Get(rel.Name, res.Version) + r, err := instAction.cfg.Releases.Get(rel.Name, res.Version) + is.NoError(err) + getres, err := releaserToV1Release(r) is.NoError(err) - is.Equal(getres.Info.Status, release.StatusDeployed) + is.Equal(getres.Info.Status, rcommon.StatusDeployed) } func TestInstallRelease_KubeVersion(t *testing.T) { @@ -534,7 +559,7 @@ func TestInstallRelease_Wait(t *testing.T) { res, err := instAction.Run(buildChart(), vals) is.Error(err) is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) + is.Equal(res.Info.Status, rcommon.StatusFailed) is.Equal(goroutines, instAction.getGoroutineCount()) } @@ -575,7 +600,7 @@ func TestInstallRelease_WaitForJobs(t *testing.T) { res, err := instAction.Run(buildChart(), vals) is.Error(err) is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) + is.Equal(res.Info.Status, rcommon.StatusFailed) } func TestInstallRelease_RollbackOnFailure(t *testing.T) { diff --git a/pkg/action/list.go b/pkg/action/list.go index c6d6f2037..ea743db60 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/labels" + ri "helm.sh/helm/v4/pkg/release" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" ) @@ -145,7 +146,7 @@ func NewList(cfg *Configuration) *List { } // Run executes the list command, returning a set of matches. -func (l *List) Run() ([]*release.Release, error) { +func (l *List) Run() ([]ri.Releaser, error) { if err := l.cfg.KubeClient.IsReachable(); err != nil { return nil, err } @@ -159,9 +160,13 @@ func (l *List) Run() ([]*release.Release, error) { } } - results, err := l.cfg.Releases.List(func(rel *release.Release) bool { + results, err := l.cfg.Releases.List(func(rel ri.Releaser) bool { + r, err := releaserToV1Release(rel) + if err != nil { + return false + } // Skip anything that doesn't match the filter. - if filter != nil && !filter.MatchString(rel.Name) { + if filter != nil && !filter.MatchString(r.Name) { return false } @@ -176,30 +181,35 @@ func (l *List) Run() ([]*release.Release, error) { return results, nil } + rresults, err := releaseListToV1List(results) + if err != nil { + return nil, err + } + // by definition, superseded releases are never shown if // only the latest releases are returned. so if requested statemask // is _only_ ListSuperseded, skip the latest release filter if l.StateMask != ListSuperseded { - results = filterLatestReleases(results) + rresults = filterLatestReleases(rresults) } // State mask application must occur after filtering to // latest releases, otherwise outdated entries can be returned - results = l.filterStateMask(results) + rresults = l.filterStateMask(rresults) // Skip anything that doesn't match the selector selectorObj, err := labels.Parse(l.Selector) if err != nil { return nil, err } - results = l.filterSelector(results, selectorObj) + rresults = l.filterSelector(rresults, selectorObj) // Unfortunately, we have to sort before truncating, which can incur substantial overhead - l.sort(results) + l.sort(rresults) // Guard on offset - if l.Offset >= len(results) { - return []*release.Release{}, nil + if l.Offset >= len(rresults) { + return releaseV1ListToReleaserList([]*release.Release{}) } // Calculate the limit and offset, and then truncate results if necessary. @@ -208,12 +218,12 @@ func (l *List) Run() ([]*release.Release, error) { limit = l.Limit } last := l.Offset + limit - if l := len(results); l < last { + if l := len(rresults); l < last { last = l } - results = results[l.Offset:last] + rresults = rresults[l.Offset:last] - return results, err + return releaseV1ListToReleaserList(rresults) } // sort is an in-place sort where order is based on the value of a.Sort diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index 75737d635..bf34b0ba1 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -24,6 +24,8 @@ import ( "github.com/stretchr/testify/assert" kubefake "helm.sh/helm/v4/pkg/kube/fake" + ri "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage" ) @@ -96,8 +98,11 @@ func TestList_Sort(t *testing.T) { lister := newListFixture(t) lister.Sort = ByNameDesc // Other sorts are tested elsewhere makeMeSomeReleases(t, lister.cfg.Releases) - list, err := lister.Run() + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) + is.Len(list, 3) is.Equal("two", list[0].Name) is.Equal("three", list[1].Name) @@ -109,7 +114,9 @@ func TestList_Limit(t *testing.T) { lister := newListFixture(t) lister.Limit = 2 makeMeSomeReleases(t, lister.cfg.Releases) - list, err := lister.Run() + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) is.Len(list, 2) // Lex order means one, three, two @@ -122,7 +129,9 @@ func TestList_BigLimit(t *testing.T) { lister := newListFixture(t) lister.Limit = 20 makeMeSomeReleases(t, lister.cfg.Releases) - list, err := lister.Run() + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) is.Len(list, 3) @@ -138,7 +147,9 @@ func TestList_LimitOffset(t *testing.T) { lister.Limit = 2 lister.Offset = 1 makeMeSomeReleases(t, lister.cfg.Releases) - list, err := lister.Run() + l, err := lister.Run() + is.NoError(err) + list, err := releaseListToV1List(l) is.NoError(err) is.Len(list, 2) @@ -168,23 +179,42 @@ func TestList_StateMask(t *testing.T) { is := assert.New(t) lister := newListFixture(t) makeMeSomeReleases(t, lister.cfg.Releases) - one, err := lister.cfg.Releases.Get("one", 1) + oner, err := lister.cfg.Releases.Get("one", 1) is.NoError(err) - one.SetStatus(release.StatusUninstalled, "uninstalled") + + var one release.Release + switch v := oner.(type) { + case release.Release: + one = v + case *release.Release: + one = *v + default: + t.Fatal("unsupported release type") + } + + one.SetStatus(common.StatusUninstalled, "uninstalled") err = lister.cfg.Releases.Update(one) is.NoError(err) res, err := lister.Run() is.NoError(err) is.Len(res, 2) - is.Equal("three", res[0].Name) - is.Equal("two", res[1].Name) + + ac0, err := ri.NewAccessor(res[0]) + is.NoError(err) + ac1, err := ri.NewAccessor(res[1]) + is.NoError(err) + + is.Equal("three", ac0.Name()) + is.Equal("two", ac1.Name()) lister.StateMask = ListUninstalled res, err = lister.Run() is.NoError(err) is.Len(res, 1) - is.Equal("one", res[0].Name) + ac0, err = ri.NewAccessor(res[0]) + is.NoError(err) + is.Equal("one", ac0.Name()) lister.StateMask |= ListDeployed res, err = lister.Run() @@ -206,28 +236,30 @@ func TestList_StateMaskWithStaleRevisions(t *testing.T) { // "dirty" release should _not_ be present as most recent // release is deployed despite failed release in past - is.Equal("failed", res[0].Name) + ac0, err := ri.NewAccessor(res[0]) + is.NoError(err) + is.Equal("failed", ac0.Name()) } func makeMeSomeReleasesWithStaleFailure(t *testing.T, store *storage.Storage) { t.Helper() - one := namedReleaseStub("clean", release.StatusDeployed) + one := namedReleaseStub("clean", common.StatusDeployed) one.Namespace = "default" one.Version = 1 - two := namedReleaseStub("dirty", release.StatusDeployed) + two := namedReleaseStub("dirty", common.StatusDeployed) two.Namespace = "default" two.Version = 1 - three := namedReleaseStub("dirty", release.StatusFailed) + three := namedReleaseStub("dirty", common.StatusFailed) three.Namespace = "default" three.Version = 2 - four := namedReleaseStub("dirty", release.StatusDeployed) + four := namedReleaseStub("dirty", common.StatusDeployed) four.Namespace = "default" four.Version = 3 - five := namedReleaseStub("failed", release.StatusFailed) + five := namedReleaseStub("failed", common.StatusFailed) five.Namespace = "default" five.Version = 1 @@ -251,7 +283,9 @@ func TestList_Filter(t *testing.T) { res, err := lister.Run() is.NoError(err) is.Len(res, 1) - is.Equal("three", res[0].Name) + ac0, err := ri.NewAccessor(res[0]) + is.NoError(err) + is.Equal("three", ac0.Name()) } func TestList_FilterFailsCompile(t *testing.T) { diff --git a/pkg/action/release_testing.go b/pkg/action/release_testing.go index d00c8d8c0..b649579f4 100644 --- a/pkg/action/release_testing.go +++ b/pkg/action/release_testing.go @@ -28,6 +28,7 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" + ri "helm.sh/helm/v4/pkg/release" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -56,7 +57,7 @@ func NewReleaseTesting(cfg *Configuration) *ReleaseTesting { } // Run executes 'helm test' against the given release. -func (r *ReleaseTesting) Run(name string) (*release.Release, error) { +func (r *ReleaseTesting) Run(name string) (ri.Releaser, error) { if err := r.cfg.KubeClient.IsReachable(); err != nil { return nil, err } @@ -66,7 +67,12 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) { } // finds the non-deleted release with the given name - rel, err := r.cfg.Releases.Last(name) + reli, err := r.cfg.Releases.Last(name) + if err != nil { + return reli, err + } + + rel, err := releaserToV1Release(reli) if err != nil { return rel, err } diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index ade3ab233..a146e0e73 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -25,6 +25,7 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -111,7 +112,12 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele return nil, nil, false, errInvalidRevision } - currentRelease, err := r.cfg.Releases.Last(name) + currentReleasei, err := r.cfg.Releases.Last(name) + if err != nil { + return nil, nil, false, err + } + + currentRelease, err := releaserToV1Release(currentReleasei) if err != nil { return nil, nil, false, err } @@ -128,7 +134,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele // Check if the history version to be rolled back exists previousVersionExist := false - for _, historyRelease := range historyReleases { + for _, historyReleasei := range historyReleases { + historyRelease, err := releaserToV1Release(historyReleasei) + if err != nil { + return nil, nil, false, err + } version := historyRelease.Version if previousVersion == version { previousVersionExist = true @@ -141,7 +151,11 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion) - previousRelease, err := r.cfg.Releases.Get(name, previousVersion) + previousReleasei, err := r.cfg.Releases.Get(name, previousVersion) + if err != nil { + return nil, nil, false, err + } + previousRelease, err := releaserToV1Release(previousReleasei) if err != nil { return nil, nil, false, err } @@ -160,7 +174,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele Info: &release.Info{ FirstDeployed: currentRelease.Info.FirstDeployed, LastDeployed: time.Now(), - Status: release.StatusPendingRollback, + Status: common.StatusPendingRollback, Notes: previousRelease.Info.Notes, // Because we lose the reference to previous version elsewhere, we set the // message here, and only override it later if we experience failure. @@ -217,8 +231,8 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas if err != nil { msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) slog.Warn(msg) - currentRelease.Info.Status = release.StatusSuperseded - targetRelease.Info.Status = release.StatusFailed + currentRelease.Info.Status = common.StatusSuperseded + targetRelease.Info.Status = common.StatusFailed targetRelease.Info.Description = msg r.cfg.recordRelease(currentRelease) r.cfg.recordRelease(targetRelease) @@ -241,14 +255,14 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas } if r.WaitForJobs { if err := waiter.WaitWithJobs(target, r.Timeout); err != nil { - targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) + targetRelease.SetStatus(common.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) r.cfg.recordRelease(currentRelease) r.cfg.recordRelease(targetRelease) return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err) } } else { if err := waiter.Wait(target, r.Timeout); err != nil { - targetRelease.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) + targetRelease.SetStatus(common.StatusFailed, fmt.Sprintf("Release %q failed: %s", targetRelease.Name, err.Error())) r.cfg.recordRelease(currentRelease) r.cfg.recordRelease(targetRelease) return targetRelease, fmt.Errorf("release %s failed: %w", targetRelease.Name, err) @@ -267,13 +281,17 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas return nil, err } // Supersede all previous deployments, see issue #2941. - for _, rel := range deployed { + for _, reli := range deployed { + rel, err := releaserToV1Release(reli) + if err != nil { + return nil, err + } slog.Debug("superseding previous deployment", "version", rel.Version) - rel.Info.Status = release.StatusSuperseded + rel.Info.Status = common.StatusSuperseded r.cfg.recordRelease(rel) } - targetRelease.Info.Status = release.StatusDeployed + targetRelease.Info.Status = common.StatusDeployed return targetRelease, nil } diff --git a/pkg/action/status.go b/pkg/action/status.go index 509c52cd9..a9f2dcd4f 100644 --- a/pkg/action/status.go +++ b/pkg/action/status.go @@ -50,7 +50,12 @@ func (s *Status) Run(name string) (*release.Release, error) { return nil, err } - rel, err := s.cfg.releaseContent(name, s.Version) + reli, err := s.cfg.releaseContent(name, s.Version) + if err != nil { + return nil, err + } + + rel, err := releaserToV1Release(reli) if err != nil { return nil, err } diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 057c2118f..669431741 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -27,6 +27,7 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage/driver" @@ -67,13 +68,18 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) } if u.DryRun { - r, err := u.cfg.releaseContent(name, 0) + ri, err := u.cfg.releaseContent(name, 0) + if err != nil { if u.IgnoreNotFound && errors.Is(err, driver.ErrReleaseNotFound) { return nil, nil } return &release.UninstallReleaseResponse{}, err } + r, err := releaserToV1Release(ri) + if err != nil { + return nil, err + } return &release.UninstallReleaseResponse{Release: r}, nil } @@ -81,23 +87,28 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) return nil, fmt.Errorf("uninstall: Release name is invalid: %s", name) } - rels, err := u.cfg.Releases.History(name) + relsi, err := u.cfg.Releases.History(name) if err != nil { if u.IgnoreNotFound { return nil, nil } return nil, fmt.Errorf("uninstall: Release not loaded: %s: %w", name, err) } - if len(rels) < 1 { + if len(relsi) < 1 { return nil, errMissingRelease } + rels, err := releaseListToV1List(relsi) + if err != nil { + return nil, err + } + releaseutil.SortByRevision(rels) rel := rels[len(rels)-1] // TODO: Are there any cases where we want to force a delete even if it's // already marked deleted? - if rel.Info.Status == release.StatusUninstalled { + if rel.Info.Status == common.StatusUninstalled { if !u.KeepHistory { if err := u.purgeReleases(rels...); err != nil { return nil, fmt.Errorf("uninstall: Failed to purge the release: %w", err) @@ -108,7 +119,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) } slog.Debug("uninstall: deleting release", "name", name) - rel.Info.Status = release.StatusUninstalling + rel.Info.Status = common.StatusUninstalling rel.Info.Deleted = time.Now() rel.Info.Description = "Deletion in progress (or silently failed)" res := &release.UninstallReleaseResponse{Release: rel} @@ -150,7 +161,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) } } - rel.Info.Status = release.StatusUninstalled + rel.Info.Status = common.StatusUninstalled if len(u.Description) > 0 { rel.Info.Description = u.Description } else { diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 7c7344383..667e96820 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -27,7 +27,7 @@ import ( "helm.sh/helm/v4/pkg/kube" kubefake "helm.sh/helm/v4/pkg/kube/fake" - release "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/release/common" ) func uninstallAction(t *testing.T) *Uninstall { @@ -119,7 +119,7 @@ func TestUninstallRelease_Wait(t *testing.T) { res, err := unAction.Run(rel.Name) is.Error(err) is.Contains(err.Error(), "U timed out") - is.Equal(res.Release.Info.Status, release.StatusUninstalled) + is.Equal(res.Release.Info.Status, common.StatusUninstalled) } func TestUninstallRelease_Cascade(t *testing.T) { diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 8dfc8b206..dc3761761 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -36,6 +36,7 @@ import ( "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/registry" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage/driver" @@ -219,7 +220,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str } // finds the last non-deleted release with the given name - lastRelease, err := u.cfg.Releases.Last(name) + lastReleasei, err := u.cfg.Releases.Last(name) if err != nil { // to keep existing behavior of returning the "%q has no deployed releases" error when an existing release does not exist if errors.Is(err, driver.ErrReleaseNotFound) { @@ -228,26 +229,37 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } + lastRelease, err := releaserToV1Release(lastReleasei) + if err != nil { + return nil, nil, false, err + } + // Concurrent `helm upgrade`s will either fail here with `errPending` or when creating the release with "already exists". This should act as a pessimistic lock. if lastRelease.Info.Status.IsPending() { return nil, nil, false, errPending } var currentRelease *release.Release - if lastRelease.Info.Status == release.StatusDeployed { + if lastRelease.Info.Status == rcommon.StatusDeployed { // no need to retrieve the last deployed release from storage as the last release is deployed currentRelease = lastRelease } else { // finds the deployed release with the given name - currentRelease, err = u.cfg.Releases.Deployed(name) + currentReleasei, err := u.cfg.Releases.Deployed(name) + var cerr error + currentRelease, cerr = releaserToV1Release(currentReleasei) + if cerr != nil { + return nil, nil, false, err + } if err != nil { if errors.Is(err, driver.ErrNoDeployedReleases) && - (lastRelease.Info.Status == release.StatusFailed || lastRelease.Info.Status == release.StatusSuperseded) { + (lastRelease.Info.Status == rcommon.StatusFailed || lastRelease.Info.Status == rcommon.StatusSuperseded) { currentRelease = lastRelease } else { return nil, nil, false, err } } + } // determine if values will be reused @@ -305,7 +317,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str Info: &release.Info{ FirstDeployed: currentRelease.Info.FirstDeployed, LastDeployed: Timestamper(), - Status: release.StatusPendingUpgrade, + Status: rcommon.StatusPendingUpgrade, Description: "Preparing upgrade", // This should be overwritten later. }, Version: revision, @@ -487,10 +499,10 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele } } - originalRelease.Info.Status = release.StatusSuperseded + originalRelease.Info.Status = rcommon.StatusSuperseded u.cfg.recordRelease(originalRelease) - upgradedRelease.Info.Status = release.StatusDeployed + upgradedRelease.Info.Status = rcommon.StatusDeployed if len(u.Description) > 0 { upgradedRelease.Info.Description = u.Description } else { @@ -503,7 +515,7 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err) slog.Warn("upgrade failed", "name", rel.Name, slog.Any("error", err)) - rel.Info.Status = release.StatusFailed + rel.Info.Status = rcommon.StatusFailed rel.Info.Description = msg u.cfg.recordRelease(rel) if u.CleanupOnFail && len(created) > 0 { @@ -533,12 +545,16 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e return rel, fmt.Errorf("an error occurred while finding last successful release. original upgrade error: %w: %w", err, herr) } + fullHistoryV1, herr := releaseListToV1List(fullHistory) + if herr != nil { + return nil, herr + } // There isn't a way to tell if a previous release was successful, but // generally failed releases do not get superseded unless the next // release is successful, so this should be relatively safe filteredHistory := releaseutil.FilterFunc(func(r *release.Release) bool { - return r.Info.Status == release.StatusSuperseded || r.Info.Status == release.StatusDeployed - }).Filter(fullHistory) + return r.Info.Status == rcommon.StatusSuperseded || r.Info.Status == rcommon.StatusDeployed + }).Filter(fullHistoryV1) if len(filteredHistory) == 0 { return rel, fmt.Errorf("unable to find a previously successful release when attempting to rollback. original upgrade error: %w", err) } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index c7930c769..67320b207 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -33,6 +33,7 @@ import ( "github.com/stretchr/testify/require" kubefake "helm.sh/helm/v4/pkg/kube/fake" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -52,7 +53,7 @@ func TestUpgradeRelease_Success(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "previous-release" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed req.NoError(upAction.cfg.Releases.Create(rel)) upAction.WaitStrategy = kube.StatusWatcherStrategy @@ -61,15 +62,17 @@ func TestUpgradeRelease_Success(t *testing.T) { ctx, done := context.WithCancel(t.Context()) res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) req.NoError(err) - is.Equal(res.Info.Status, release.StatusDeployed) + is.Equal(res.Info.Status, common.StatusDeployed) done() // Detecting previous bug where context termination after successful release // caused release to fail. time.Sleep(time.Millisecond * 100) - lastRelease, err := upAction.cfg.Releases.Last(rel.Name) + lastReleasei, err := upAction.cfg.Releases.Last(rel.Name) req.NoError(err) - is.Equal(lastRelease.Info.Status, release.StatusDeployed) + lastRelease, err := releaserToV1Release(lastReleasei) + req.NoError(err) + is.Equal(lastRelease.Info.Status, common.StatusDeployed) } func TestUpgradeRelease_Wait(t *testing.T) { @@ -79,7 +82,7 @@ func TestUpgradeRelease_Wait(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -91,7 +94,7 @@ func TestUpgradeRelease_Wait(t *testing.T) { res, err := upAction.Run(rel.Name, buildChart(), vals) req.Error(err) is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) + is.Equal(res.Info.Status, common.StatusFailed) } func TestUpgradeRelease_WaitForJobs(t *testing.T) { @@ -101,7 +104,7 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -114,7 +117,7 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) { res, err := upAction.Run(rel.Name, buildChart(), vals) req.Error(err) is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) + is.Equal(res.Info.Status, common.StatusFailed) } func TestUpgradeRelease_CleanupOnFail(t *testing.T) { @@ -124,7 +127,7 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -139,7 +142,7 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) { req.Error(err) is.NotContains(err.Error(), "unable to cleanup resources") is.Contains(res.Info.Description, "I timed out") - is.Equal(res.Info.Status, release.StatusFailed) + is.Equal(res.Info.Status, common.StatusFailed) } func TestUpgradeRelease_RollbackOnFailure(t *testing.T) { @@ -151,7 +154,7 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) { rel := releaseStub() rel.Name = "nuketown" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -167,17 +170,19 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) { is.Contains(err.Error(), "rollback-on-failure") // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) + updatedResi, err := upAction.cfg.Releases.Get(res.Name, 3) + is.NoError(err) + updatedRes, err := releaserToV1Release(updatedResi) is.NoError(err) // Should have rolled back to the previous - is.Equal(updatedRes.Info.Status, release.StatusDeployed) + is.Equal(updatedRes.Info.Status, common.StatusDeployed) }) t.Run("rollback-on-failure uninstall fails", func(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "fallout" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -218,7 +223,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { rel := releaseStub() rel.Name = "nuketown" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed rel.Config = existingValues err := upAction.cfg.Releases.Create(rel) @@ -230,14 +235,17 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { is.NoError(err) // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) + updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2) is.NoError(err) - if updatedRes == nil { + if updatedResi == nil { is.Fail("Updated Release is nil") return } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) + updatedRes, err := releaserToV1Release(updatedResi) + is.NoError(err) + + is.Equal(common.StatusDeployed, updatedRes.Info.Status) is.Equal(expectedValues, updatedRes.Config) }) @@ -270,7 +278,7 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { Info: &release.Info{ FirstDeployed: now, LastDeployed: now, - Status: release.StatusDeployed, + Status: common.StatusDeployed, Description: "Named Release Stub", }, Chart: sampleChart, @@ -292,14 +300,17 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { is.NoError(err) // Now get the upgraded release - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) + updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2) is.NoError(err) - if updatedRes == nil { + if updatedResi == nil { is.Fail("Updated Release is nil") return } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) + updatedRes, err := releaserToV1Release(updatedResi) + is.NoError(err) + + is.Equal(common.StatusDeployed, updatedRes.Info.Status) is.Equal(0, len(updatedRes.Chart.Dependencies()), "expected 0 dependencies") expectedValues := map[string]interface{}{ @@ -339,7 +350,7 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) { rel := releaseStub() rel.Name = "nuketown" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed rel.Config = existingValues err := upAction.cfg.Releases.Create(rel) @@ -351,14 +362,17 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) { is.NoError(err) // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) + updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2) is.NoError(err) - if updatedRes == nil { + if updatedResi == nil { is.Fail("Updated Release is nil") return } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) + updatedRes, err := releaserToV1Release(updatedResi) + is.NoError(err) + + is.Equal(common.StatusDeployed, updatedRes.Info.Status) is.Equal(expectedValues, updatedRes.Config) is.Equal(newChartValues, updatedRes.Chart.Values) }) @@ -370,11 +384,11 @@ func TestUpgradeRelease_Pending(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "come-fail-away" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) rel2 := releaseStub() rel2.Name = "come-fail-away" - rel2.Info.Status = release.StatusPendingUpgrade + rel2.Info.Status = common.StatusPendingUpgrade rel2.Version = 2 upAction.cfg.Releases.Create(rel2) @@ -391,7 +405,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "interrupted-release" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -407,7 +421,7 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { req.Error(err) is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled") - is.Equal(res.Info.Status, release.StatusFailed) + is.Equal(res.Info.Status, common.StatusFailed) } func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) { @@ -418,7 +432,7 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "interrupted-release" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed upAction.cfg.Releases.Create(rel) failer := upAction.cfg.KubeClient.(*kubefake.FailingKubeClient) @@ -436,10 +450,12 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) { is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to rollback-on-failure being set: context canceled") // Now make sure it is actually upgraded - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 3) + updatedResi, err := upAction.cfg.Releases.Get(res.Name, 3) + is.NoError(err) + updatedRes, err := releaserToV1Release(updatedResi) is.NoError(err) // Should have rolled back to the previous - is.Equal(updatedRes.Info.Status, release.StatusDeployed) + is.Equal(updatedRes.Info.Status, common.StatusDeployed) } func TestMergeCustomLabels(t *testing.T) { @@ -468,7 +484,7 @@ func TestUpgradeRelease_Labels(t *testing.T) { "key1": "val1", "key2": "val2.1", } - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed err := upAction.cfg.Releases.Create(rel) is.NoError(err) @@ -483,25 +499,29 @@ func TestUpgradeRelease_Labels(t *testing.T) { is.NoError(err) // Now make sure it is actually upgraded and labels were merged - updatedRes, err := upAction.cfg.Releases.Get(res.Name, 2) + updatedResi, err := upAction.cfg.Releases.Get(res.Name, 2) is.NoError(err) - if updatedRes == nil { + if updatedResi == nil { is.Fail("Updated Release is nil") return } - is.Equal(release.StatusDeployed, updatedRes.Info.Status) + updatedRes, err := releaserToV1Release(updatedResi) + is.NoError(err) + is.Equal(common.StatusDeployed, updatedRes.Info.Status) is.Equal(mergeCustomLabels(rel.Labels, upAction.Labels), updatedRes.Labels) // Now make sure it is suppressed release still contains original labels - initialRes, err := upAction.cfg.Releases.Get(res.Name, 1) + initialResi, err := upAction.cfg.Releases.Get(res.Name, 1) is.NoError(err) - if initialRes == nil { + if initialResi == nil { is.Fail("Updated Release is nil") return } - is.Equal(initialRes.Info.Status, release.StatusSuperseded) + initialRes, err := releaserToV1Release(initialResi) + is.NoError(err) + is.Equal(initialRes.Info.Status, common.StatusSuperseded) is.Equal(initialRes.Labels, rel.Labels) } @@ -516,7 +536,7 @@ func TestUpgradeRelease_SystemLabels(t *testing.T) { "key1": "val1", "key2": "val2.1", } - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed err := upAction.cfg.Releases.Create(rel) is.NoError(err) @@ -542,7 +562,7 @@ func TestUpgradeRelease_DryRun(t *testing.T) { upAction := upgradeAction(t) rel := releaseStub() rel.Name = "previous-release" - rel.Info.Status = release.StatusDeployed + rel.Info.Status = common.StatusDeployed req.NoError(upAction.cfg.Releases.Create(rel)) upAction.DryRunStrategy = DryRunClient @@ -552,12 +572,14 @@ func TestUpgradeRelease_DryRun(t *testing.T) { res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) done() req.NoError(err) - is.Equal(release.StatusPendingUpgrade, res.Info.Status) + is.Equal(common.StatusPendingUpgrade, res.Info.Status) is.Contains(res.Manifest, "kind: Secret") - lastRelease, err := upAction.cfg.Releases.Last(rel.Name) + lastReleasei, err := upAction.cfg.Releases.Last(rel.Name) + req.NoError(err) + lastRelease, err := releaserToV1Release(lastReleasei) req.NoError(err) - is.Equal(lastRelease.Info.Status, release.StatusDeployed) + is.Equal(lastRelease.Info.Status, common.StatusDeployed) is.Equal(1, lastRelease.Version) // Test the case for hiding the secret to ensure it is not displayed @@ -568,12 +590,14 @@ func TestUpgradeRelease_DryRun(t *testing.T) { res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) done() req.NoError(err) - is.Equal(release.StatusPendingUpgrade, res.Info.Status) + is.Equal(common.StatusPendingUpgrade, res.Info.Status) is.NotContains(res.Manifest, "kind: Secret") - lastRelease, err = upAction.cfg.Releases.Last(rel.Name) + lastReleasei, err = upAction.cfg.Releases.Last(rel.Name) + req.NoError(err) + lastRelease, err = releaserToV1Release(lastReleasei) req.NoError(err) - is.Equal(lastRelease.Info.Status, release.StatusDeployed) + is.Equal(lastRelease.Info.Status, common.StatusDeployed) is.Equal(1, lastRelease.Version) // Ensure in a dry run mode when using HideSecret diff --git a/pkg/cmd/completion_test.go b/pkg/cmd/completion_test.go index 375a9a97d..81c1ee2ad 100644 --- a/pkg/cmd/completion_test.go +++ b/pkg/cmd/completion_test.go @@ -22,6 +22,7 @@ import ( "testing" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -31,7 +32,7 @@ func checkFileCompletion(t *testing.T, cmdName string, shouldBePerformed bool) { storage := storageFixture() storage.Create(&release.Release{ Name: "myrelease", - Info: &release.Info{Status: release.StatusDeployed}, + Info: &release.Info{Status: common.StatusDeployed}, Chart: &chart.Chart{ Metadata: &chart.Metadata{ Name: "Myrelease-Chart", diff --git a/pkg/cmd/flags_test.go b/pkg/cmd/flags_test.go index 8d79716f0..614970252 100644 --- a/pkg/cmd/flags_test.go +++ b/pkg/cmd/flags_test.go @@ -25,6 +25,7 @@ import ( "helm.sh/helm/v4/pkg/action" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -64,35 +65,35 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) { cmd: fmt.Sprintf("__complete %s --output ''", cmdName), golden: "output/output-comp.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }), }, { name: "completion for output flag long and after arg", cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName), golden: "output/output-comp.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }), }, { name: "completion for output flag short and before arg", cmd: fmt.Sprintf("__complete %s -o ''", cmdName), golden: "output/output-comp.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }), }, { name: "completion for output flag short and after arg", cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName), golden: "output/output-comp.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }), }, { name: "completion for output flag, no filter", cmd: fmt.Sprintf("__complete %s --output jso", cmdName), golden: "output/output-comp.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }), }} runTestCmd(t, tests) diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index f4dde95e4..fc3c26b02 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -111,7 +111,11 @@ func (r releaseHistory) WriteTable(out io.Writer) error { } func getHistory(client *action.History, name string) (releaseHistory, error) { - hist, err := client.Run(name) + histi, err := client.Run(name) + if err != nil { + return nil, err + } + hist, err := releaseListToV1List(histi) if err != nil { return nil, err } @@ -180,7 +184,11 @@ func compListRevisions(_ string, cfg *action.Configuration, releaseName string) client := action.NewHistory(cfg) var revisions []string - if hist, err := client.Run(releaseName); err == nil { + if histi, err := client.Run(releaseName); err == nil { + hist, err := releaseListToV1List(histi) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } for _, version := range hist { appVersion := fmt.Sprintf("App: %s", version.Chart.Metadata.AppVersion) chartDesc := fmt.Sprintf("Chart: %s-%s", version.Chart.Metadata.Name, version.Chart.Metadata.Version) diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index d26ed9ecf..a324e8bdd 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -20,11 +20,12 @@ import ( "fmt" "testing" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) func TestHistoryCmd(t *testing.T) { - mk := func(name string, vers int, status release.Status) *release.Release { + mk := func(name string, vers int, status common.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{ Name: name, Version: vers, @@ -36,34 +37,34 @@ func TestHistoryCmd(t *testing.T) { name: "get history for release", cmd: "history angry-bird", rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), - mk("angry-bird", 2, release.StatusSuperseded), - mk("angry-bird", 1, release.StatusSuperseded), + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), + mk("angry-bird", 2, common.StatusSuperseded), + mk("angry-bird", 1, common.StatusSuperseded), }, golden: "output/history.txt", }, { name: "get history with max limit set", cmd: "history angry-bird --max 2", rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), }, golden: "output/history-limit.txt", }, { name: "get history with yaml output format", cmd: "history angry-bird --output yaml", rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), }, golden: "output/history.yaml", }, { name: "get history with json output format", cmd: "history angry-bird --output json", rels: []*release.Release{ - mk("angry-bird", 4, release.StatusDeployed), - mk("angry-bird", 3, release.StatusSuperseded), + mk("angry-bird", 4, common.StatusDeployed), + mk("angry-bird", 3, common.StatusSuperseded), }, golden: "output/history.json", }} @@ -76,7 +77,7 @@ func TestHistoryOutputCompletion(t *testing.T) { func revisionFlagCompletionTest(t *testing.T, cmdName string) { t.Helper() - mk := func(name string, vers int, status release.Status) *release.Release { + mk := func(name string, vers int, status common.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{ Name: name, Version: vers, @@ -85,10 +86,10 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) { } releases := []*release.Release{ - mk("musketeers", 11, release.StatusDeployed), - mk("musketeers", 10, release.StatusSuperseded), - mk("musketeers", 9, release.StatusSuperseded), - mk("musketeers", 8, release.StatusSuperseded), + mk("musketeers", 11, common.StatusDeployed), + mk("musketeers", 10, common.StatusSuperseded), + mk("musketeers", 9, common.StatusSuperseded), + mk("musketeers", 8, common.StatusSuperseded), } tests := []cmdTestCase{{ diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 55d828036..99e0bc9b8 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -30,6 +30,7 @@ import ( "helm.sh/helm/v4/pkg/action" "helm.sh/helm/v4/pkg/cli/output" "helm.sh/helm/v4/pkg/cmd/require" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -79,7 +80,11 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } client.SetStateMask() - results, err := client.Run() + resultsi, err := client.Run() + if err != nil { + return err + } + results, err := releaseListToV1List(resultsi) if err != nil { return err } @@ -193,28 +198,28 @@ func (w *releaseListWriter) WriteTable(out io.Writer) error { } for _, r := range w.releases { // Parse the status string back to a release.Status to use color - var status release.Status + var status common.Status switch r.Status { case "deployed": - status = release.StatusDeployed + status = common.StatusDeployed case "failed": - status = release.StatusFailed + status = common.StatusFailed case "pending-install": - status = release.StatusPendingInstall + status = common.StatusPendingInstall case "pending-upgrade": - status = release.StatusPendingUpgrade + status = common.StatusPendingUpgrade case "pending-rollback": - status = release.StatusPendingRollback + status = common.StatusPendingRollback case "uninstalling": - status = release.StatusUninstalling + status = common.StatusUninstalling case "uninstalled": - status = release.StatusUninstalled + status = common.StatusUninstalled case "superseded": - status = release.StatusSuperseded + status = common.StatusSuperseded case "unknown": - status = release.StatusUnknown + status = common.StatusUnknown default: - status = release.Status(r.Status) + status = common.Status(r.Status) } table.AddRow(r.Name, coloroutput.ColorizeNamespace(r.Namespace, w.noColor), r.Revision, r.Updated, coloroutput.ColorizeStatus(status, w.noColor), r.Chart, r.AppVersion) } @@ -264,7 +269,11 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti // client.Filter = fmt.Sprintf("^%s", toComplete) client.SetStateMask() - releases, err := client.Run() + releasesi, err := client.Run() + if err != nil { + return nil, cobra.ShellCompDirectiveDefault + } + releases, err := releaseListToV1List(releasesi) if err != nil { return nil, cobra.ShellCompDirectiveDefault } diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index 22a948fff..42e9e2083 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -21,6 +21,7 @@ import ( "time" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -47,7 +48,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusSuperseded, + Status: common.StatusSuperseded, }, Chart: chartInfo, }, @@ -57,7 +58,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: chartInfo, }, @@ -67,7 +68,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusUninstalled, + Status: common.StatusUninstalled, }, Chart: chartInfo, }, @@ -77,7 +78,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusSuperseded, + Status: common.StatusSuperseded, }, Chart: chartInfo, }, @@ -87,7 +88,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp2, - Status: release.StatusFailed, + Status: common.StatusFailed, }, Chart: chartInfo, }, @@ -97,7 +98,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusUninstalling, + Status: common.StatusUninstalling, }, Chart: chartInfo, }, @@ -107,7 +108,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusPendingInstall, + Status: common.StatusPendingInstall, }, Chart: chartInfo, }, @@ -117,7 +118,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp3, - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: chartInfo, }, @@ -127,7 +128,7 @@ func TestListCmd(t *testing.T) { Namespace: defaultNamespace, Info: &release.Info{ LastDeployed: timestamp4, - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: chartInfo, }, @@ -137,7 +138,7 @@ func TestListCmd(t *testing.T) { Namespace: "milano", Info: &release.Info{ LastDeployed: timestamp1, - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: chartInfo, }, diff --git a/pkg/cmd/release_testing.go b/pkg/cmd/release_testing.go index e02604f63..88a6f351f 100644 --- a/pkg/cmd/release_testing.go +++ b/pkg/cmd/release_testing.go @@ -65,13 +65,17 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command client.Filters[action.ExcludeNameFilter] = append(client.Filters[action.ExcludeNameFilter], notName.ReplaceAllLiteralString(f, "")) } } - rel, runErr := client.Run(args[0]) + reli, runErr := client.Run(args[0]) // We only return an error if we weren't even able to get the // release, otherwise we keep going so we can print status and logs // if requested - if runErr != nil && rel == nil { + if runErr != nil && reli == nil { return runErr } + rel, err := releaserToV1Release(reli) + if err != nil { + return err + } if err := outfmt.Write(out, &statusPrinter{ release: rel, diff --git a/pkg/cmd/release_testing_test.go b/pkg/cmd/release_testing_test.go index 0c46755ac..fdb5df1e9 100644 --- a/pkg/cmd/release_testing_test.go +++ b/pkg/cmd/release_testing_test.go @@ -26,6 +26,7 @@ import ( "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" kubefake "helm.sh/helm/v4/pkg/kube/fake" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -46,7 +47,7 @@ func TestReleaseTestNotesHandling(t *testing.T) { Name: "test-release", Namespace: "default", Info: &release.Info{ - Status: release.StatusDeployed, + Status: rcommon.StatusDeployed, Notes: "Some important notes that should be hidden by default", }, Chart: &chart.Chart{Metadata: &chart.Metadata{Name: "test", Version: "1.0.0"}}, diff --git a/pkg/cmd/rollback_test.go b/pkg/cmd/rollback_test.go index 53c63613e..116e158fd 100644 --- a/pkg/cmd/rollback_test.go +++ b/pkg/cmd/rollback_test.go @@ -22,6 +22,7 @@ import ( "testing" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -29,13 +30,13 @@ func TestRollbackCmd(t *testing.T) { rels := []*release.Release{ { Name: "funny-honey", - Info: &release.Info{Status: release.StatusSuperseded}, + Info: &release.Info{Status: common.StatusSuperseded}, Chart: &chart.Chart{}, Version: 1, }, { Name: "funny-honey", - Info: &release.Info{Status: release.StatusDeployed}, + Info: &release.Info{Status: common.StatusDeployed}, Chart: &chart.Chart{}, Version: 2, }, @@ -83,7 +84,7 @@ func TestRollbackCmd(t *testing.T) { } func TestRollbackRevisionCompletion(t *testing.T) { - mk := func(name string, vers int, status release.Status) *release.Release { + mk := func(name string, vers int, status common.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{ Name: name, Version: vers, @@ -92,11 +93,11 @@ func TestRollbackRevisionCompletion(t *testing.T) { } releases := []*release.Release{ - mk("musketeers", 11, release.StatusDeployed), - mk("musketeers", 10, release.StatusSuperseded), - mk("musketeers", 9, release.StatusSuperseded), - mk("musketeers", 8, release.StatusSuperseded), - mk("carabins", 1, release.StatusSuperseded), + mk("musketeers", 11, common.StatusDeployed), + mk("musketeers", 10, common.StatusSuperseded), + mk("musketeers", 9, common.StatusSuperseded), + mk("musketeers", 8, common.StatusSuperseded), + mk("carabins", 1, common.StatusSuperseded), } tests := []cmdTestCase{{ @@ -132,14 +133,14 @@ func TestRollbackWithLabels(t *testing.T) { rels := []*release.Release{ { Name: releaseName, - Info: &release.Info{Status: release.StatusSuperseded}, + Info: &release.Info{Status: common.StatusSuperseded}, Chart: &chart.Chart{}, Version: 1, Labels: labels1, }, { Name: releaseName, - Info: &release.Info{Status: release.StatusDeployed}, + Info: &release.Info{Status: common.StatusDeployed}, Chart: &chart.Chart{}, Version: 2, Labels: labels2, @@ -155,7 +156,11 @@ func TestRollbackWithLabels(t *testing.T) { if err != nil { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := storage.Get(releaseName, 3) + updatedReli, err := storage.Get(releaseName, 3) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 4f1be88d6..48dbd760d 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -39,6 +39,7 @@ import ( "helm.sh/helm/v4/pkg/cli" kubefake "helm.sh/helm/v4/pkg/kube/fake" "helm.sh/helm/v4/pkg/registry" + ri "helm.sh/helm/v4/pkg/release" release "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/repo/v1" "helm.sh/helm/v4/pkg/storage/driver" @@ -465,3 +466,31 @@ type CommandError struct { error ExitCode int } + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel ri.Releaser) (*release.Release, error) { + switch r := rel.(type) { + case release.Release: + return &r, nil + case *release.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} + +func releaseListToV1List(ls []ri.Releaser) ([]*release.Release, error) { + rls := make([]*release.Release, 0, len(ls)) + for _, val := range ls { + rel, err := releaserToV1Release(val) + if err != nil { + return nil, err + } + rls = append(rls, rel) + } + + return rls, nil +} diff --git a/pkg/cmd/status_test.go b/pkg/cmd/status_test.go index 8c251b76b..b96a0d19a 100644 --- a/pkg/cmd/status_test.go +++ b/pkg/cmd/status_test.go @@ -21,6 +21,7 @@ import ( "time" chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -41,14 +42,14 @@ func TestStatusCmd(t *testing.T) { cmd: "status flummoxed-chickadee", golden: "output/status.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }), }, { name: "get status of a deployed release, with desc", cmd: "status flummoxed-chickadee", golden: "output/status-with-desc.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, Description: "Mock description", }), }, { @@ -56,7 +57,7 @@ func TestStatusCmd(t *testing.T) { cmd: "status flummoxed-chickadee", golden: "output/status-with-notes.txt", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, Notes: "release notes", }), }, { @@ -64,7 +65,7 @@ func TestStatusCmd(t *testing.T) { cmd: "status flummoxed-chickadee -o json", golden: "output/status.json", rels: releasesMockWithStatus(&release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, Notes: "release notes", }), }, { @@ -73,7 +74,7 @@ func TestStatusCmd(t *testing.T) { golden: "output/status-with-resources.txt", rels: releasesMockWithStatus( &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, ), }, { @@ -82,7 +83,7 @@ func TestStatusCmd(t *testing.T) { golden: "output/status-with-resources.json", rels: releasesMockWithStatus( &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, ), }, { @@ -91,7 +92,7 @@ func TestStatusCmd(t *testing.T) { golden: "output/status-with-test-suite.txt", rels: releasesMockWithStatus( &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, &release.Hook{ Name: "never-run-test", @@ -140,7 +141,7 @@ func TestStatusCompletion(t *testing.T) { Name: "athos", Namespace: "default", Info: &release.Info{ - Status: release.StatusDeployed, + Status: common.StatusDeployed, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -152,7 +153,7 @@ func TestStatusCompletion(t *testing.T) { Name: "porthos", Namespace: "default", Info: &release.Info{ - Status: release.StatusFailed, + Status: common.StatusFailed, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -164,7 +165,7 @@ func TestStatusCompletion(t *testing.T) { Name: "aramis", Namespace: "default", Info: &release.Info{ - Status: release.StatusUninstalled, + Status: common.StatusUninstalled, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ @@ -176,7 +177,7 @@ func TestStatusCompletion(t *testing.T) { Name: "dartagnan", Namespace: "gascony", Info: &release.Info{ - Status: release.StatusUnknown, + Status: common.StatusUnknown, }, Chart: &chart.Chart{ Metadata: &chart.Metadata{ diff --git a/pkg/cmd/upgrade.go b/pkg/cmd/upgrade.go index a9f834ebb..f32493e87 100644 --- a/pkg/cmd/upgrade.go +++ b/pkg/cmd/upgrade.go @@ -37,7 +37,8 @@ import ( "helm.sh/helm/v4/pkg/cmd/require" "helm.sh/helm/v4/pkg/downloader" "helm.sh/helm/v4/pkg/getter" - release "helm.sh/helm/v4/pkg/release/v1" + ri "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" "helm.sh/helm/v4/pkg/storage/driver" ) @@ -318,6 +319,11 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { return cmd } -func isReleaseUninstalled(versions []*release.Release) bool { - return len(versions) > 0 && versions[len(versions)-1].Info.Status == release.StatusUninstalled +func isReleaseUninstalled(versionsi []ri.Releaser) bool { + versions, err := releaseListToV1List(versionsi) + if err != nil { + slog.Error("cannot convert release list to v1 release list", "error", err) + return false + } + return len(versions) > 0 && versions[len(versions)-1].Info.Status == common.StatusUninstalled } diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index 9b17f187d..fd715a1fa 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -28,6 +28,7 @@ import ( chart "helm.sh/helm/v4/pkg/chart/v2" "helm.sh/helm/v4/pkg/chart/v2/loader" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" + rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" ) @@ -82,7 +83,7 @@ func TestUpgradeCmd(t *testing.T) { badDepsPath := "testdata/testcharts/chart-bad-requirements" presentDepsPath := "testdata/testcharts/chart-with-subchart-update" - relWithStatusMock := func(n string, v int, ch *chart.Chart, status release.Status) *release.Release { + relWithStatusMock := func(n string, v int, ch *chart.Chart, status rcommon.Status) *release.Release { return release.Mock(&release.MockReleaseOptions{Name: n, Version: v, Chart: ch, Status: status}) } @@ -173,20 +174,20 @@ func TestUpgradeCmd(t *testing.T) { name: "upgrade a failed release", cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), golden: "output/upgrade.txt", - rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusFailed)}, + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusFailed)}, }, { name: "upgrade a pending install release", cmd: fmt.Sprintf("upgrade funny-bunny '%s'", chartPath), golden: "output/upgrade-with-pending-install.txt", wantError: true, - rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusPendingInstall)}, + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusPendingInstall)}, }, { name: "install a previously uninstalled release with '--keep-history' using 'upgrade --install'", cmd: fmt.Sprintf("upgrade funny-bunny -i '%s'", chartPath), golden: "output/upgrade-uninstalled-with-keep-history.txt", - rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, release.StatusUninstalled)}, + rels: []*release.Release{relWithStatusMock("funny-bunny", 2, ch, rcommon.StatusUninstalled)}, }, } runTestCmd(t, tests) @@ -208,7 +209,11 @@ func TestUpgradeWithValue(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := store.Get(releaseName, 4) + updatedReli, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -235,7 +240,11 @@ func TestUpgradeWithStringValue(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := store.Get(releaseName, 4) + updatedReli, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -263,7 +272,11 @@ func TestUpgradeInstallWithSubchartNotes(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - upgradedRel, err := store.Get(releaseName, 2) + upgradedReli, err := store.Get(releaseName, 2) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + upgradedRel, err := releaserToV1Release(upgradedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -295,7 +308,11 @@ func TestUpgradeWithValuesFile(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := store.Get(releaseName, 4) + updatedReli, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -328,7 +345,11 @@ func TestUpgradeWithValuesFromStdin(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := store.Get(releaseName, 4) + updatedReli, err := store.Get(releaseName, 4) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -358,7 +379,11 @@ func TestUpgradeInstallWithValuesFromStdin(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := store.Get(releaseName, 1) + updatedReli, err := store.Get(releaseName, 1) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } @@ -463,7 +488,11 @@ func TestUpgradeInstallWithLabels(t *testing.T) { t.Errorf("unexpected error, got '%v'", err) } - updatedRel, err := store.Get(releaseName, 1) + updatedReli, err := store.Get(releaseName, 1) + if err != nil { + t.Errorf("unexpected error, got '%v'", err) + } + updatedRel, err := releaserToV1Release(updatedReli) if err != nil { t.Errorf("unexpected error, got '%v'", err) } diff --git a/pkg/release/common.go b/pkg/release/common.go index 5cdcf9b88..9e6e1050e 100644 --- a/pkg/release/common.go +++ b/pkg/release/common.go @@ -18,7 +18,10 @@ package release import ( "errors" + "fmt" + "time" + "helm.sh/helm/v4/pkg/chart" v1release "helm.sh/helm/v4/pkg/release/v1" ) @@ -33,7 +36,7 @@ func NewDefaultAccessor(rel Releaser) (Accessor, error) { case *v1release.Release: return &v1Accessor{v}, nil default: - return nil, errors.New("unsupported release type") + return nil, fmt.Errorf("unsupported release type: %T", rel) } } @@ -52,6 +55,18 @@ type v1Accessor struct { rel *v1release.Release } +func (a *v1Accessor) Name() string { + return a.rel.Name +} + +func (a *v1Accessor) Namespace() string { + return a.rel.Namespace +} + +func (a *v1Accessor) Version() int { + return a.rel.Version +} + func (a *v1Accessor) Hooks() []Hook { var hooks = make([]Hook, len(a.rel.Hooks)) for i, h := range a.rel.Hooks { @@ -68,6 +83,26 @@ func (a *v1Accessor) Notes() string { return a.rel.Info.Notes } +func (a *v1Accessor) Labels() map[string]string { + return a.rel.Labels +} + +func (a *v1Accessor) Chart() chart.Charter { + return a.rel.Chart +} + +func (a *v1Accessor) Status() string { + return a.rel.Info.Status.String() +} + +func (a *v1Accessor) ApplyMethod() string { + return a.rel.ApplyMethod +} + +func (a *v1Accessor) DeployedAt() time.Time { + return a.rel.Info.LastDeployed +} + type v1HookAccessor struct { hook *v1release.Hook } diff --git a/pkg/release/v1/status.go b/pkg/release/common/status.go similarity index 99% rename from pkg/release/v1/status.go rename to pkg/release/common/status.go index 8d6459013..fd5010301 100644 --- a/pkg/release/v1/status.go +++ b/pkg/release/common/status.go @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1 +package common // Status is the status of a release type Status string diff --git a/pkg/release/interfaces.go b/pkg/release/interfaces.go index 9d6987625..aaa5a756f 100644 --- a/pkg/release/interfaces.go +++ b/pkg/release/interfaces.go @@ -16,14 +16,28 @@ limitations under the License. package release +import ( + "time" + + "helm.sh/helm/v4/pkg/chart" +) + type Releaser interface{} type Hook interface{} type Accessor interface { + Name() string + Namespace() string + Version() int Hooks() []Hook Manifest() string Notes() string + Labels() map[string]string + Chart() chart.Charter + Status() string + ApplyMethod() string + DeployedAt() time.Time } type HookAccessor interface { diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index cef7e9960..d5b5089dc 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -18,6 +18,7 @@ package v1 import ( "time" + "helm.sh/helm/v4/pkg/release/common" "k8s.io/apimachinery/pkg/runtime" ) @@ -32,7 +33,7 @@ type Info struct { // Description is human-friendly "log entry" about this release. Description string `json:"description,omitempty"` // Status is the current state of the release - Status Status `json:"status,omitempty"` + Status common.Status `json:"status,omitempty"` // Contains the rendered templates/NOTES.txt if available Notes string `json:"notes,omitempty"` // Contains the deployed resources information diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 818cd777e..06ad90e8f 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -23,6 +23,7 @@ import ( "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" + rcommon "helm.sh/helm/v4/pkg/release/common" ) // MockHookTemplate is the hook template used for all mock release objects. @@ -45,7 +46,7 @@ type MockReleaseOptions struct { Name string Version int Chart *chart.Chart - Status Status + Status rcommon.Status Namespace string Labels map[string]string } @@ -105,7 +106,7 @@ func Mock(opts *MockReleaseOptions) *Release { } } - scode := StatusDeployed + scode := rcommon.StatusDeployed if len(opts.Status) > 0 { scode = opts.Status } diff --git a/pkg/release/v1/release.go b/pkg/release/v1/release.go index a7f076e04..454ee6eb7 100644 --- a/pkg/release/v1/release.go +++ b/pkg/release/v1/release.go @@ -17,6 +17,7 @@ package v1 import ( chart "helm.sh/helm/v4/pkg/chart/v2" + "helm.sh/helm/v4/pkg/release/common" ) type ApplyMethod string @@ -53,7 +54,7 @@ type Release struct { } // SetStatus is a helper for setting the status on a release. -func (r *Release) SetStatus(status Status, msg string) { +func (r *Release) SetStatus(status common.Status, msg string) { r.Info.Status = status r.Info.Description = msg } diff --git a/pkg/release/v1/util/filter.go b/pkg/release/v1/util/filter.go index f818a6196..dc60195cf 100644 --- a/pkg/release/v1/util/filter.go +++ b/pkg/release/v1/util/filter.go @@ -16,7 +16,10 @@ limitations under the License. package util // import "helm.sh/helm/v4/pkg/release/v1/util" -import rspb "helm.sh/helm/v4/pkg/release/v1" +import ( + "helm.sh/helm/v4/pkg/release/common" + rspb "helm.sh/helm/v4/pkg/release/v1" +) // FilterFunc returns true if the release object satisfies // the predicate of the underlying filter func. @@ -68,7 +71,7 @@ func All(filters ...FilterFunc) FilterFunc { } // StatusFilter filters a set of releases by status code. -func StatusFilter(status rspb.Status) FilterFunc { +func StatusFilter(status common.Status) FilterFunc { return FilterFunc(func(rls *rspb.Release) bool { if rls == nil { return true diff --git a/pkg/release/v1/util/filter_test.go b/pkg/release/v1/util/filter_test.go index c8b23d526..1004a4c57 100644 --- a/pkg/release/v1/util/filter_test.go +++ b/pkg/release/v1/util/filter_test.go @@ -19,20 +19,21 @@ package util // import "helm.sh/helm/v4/pkg/release/v1/util" import ( "testing" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) func TestFilterAny(t *testing.T) { - ls := Any(StatusFilter(rspb.StatusUninstalled)).Filter(releases) + ls := Any(StatusFilter(common.StatusUninstalled)).Filter(releases) if len(ls) != 2 { t.Fatalf("expected 2 results, got '%d'", len(ls)) } r0, r1 := ls[0], ls[1] switch { - case r0.Info.Status != rspb.StatusUninstalled: + case r0.Info.Status != common.StatusUninstalled: t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) - case r1.Info.Status != rspb.StatusUninstalled: + case r1.Info.Status != common.StatusUninstalled: t.Fatalf("expected UNINSTALLED result, got '%s'", r1.Info.Status.String()) } } @@ -40,7 +41,7 @@ func TestFilterAny(t *testing.T) { func TestFilterAll(t *testing.T) { fn := FilterFunc(func(rls *rspb.Release) bool { // true if not uninstalled and version < 4 - v0 := !StatusFilter(rspb.StatusUninstalled).Check(rls) + v0 := !StatusFilter(common.StatusUninstalled).Check(rls) v1 := rls.Version < 4 return v0 && v1 }) @@ -53,7 +54,7 @@ func TestFilterAll(t *testing.T) { switch r0 := ls[0]; { case r0.Version == 4: t.Fatal("got release with status revision 4") - case r0.Info.Status == rspb.StatusUninstalled: + case r0.Info.Status == common.StatusUninstalled: t.Fatal("got release with status UNINSTALLED") } } diff --git a/pkg/release/v1/util/sorter_test.go b/pkg/release/v1/util/sorter_test.go index 0889ddb94..f47db7db8 100644 --- a/pkg/release/v1/util/sorter_test.go +++ b/pkg/release/v1/util/sorter_test.go @@ -20,19 +20,20 @@ import ( "testing" "time" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) // note: this test data is shared with filter_test.go. var releases = []*rspb.Release{ - tsRelease("quiet-bear", 2, 2000, rspb.StatusSuperseded), - tsRelease("angry-bird", 4, 3000, rspb.StatusDeployed), - tsRelease("happy-cats", 1, 4000, rspb.StatusUninstalled), - tsRelease("vocal-dogs", 3, 6000, rspb.StatusUninstalled), + tsRelease("quiet-bear", 2, 2000, common.StatusSuperseded), + tsRelease("angry-bird", 4, 3000, common.StatusDeployed), + tsRelease("happy-cats", 1, 4000, common.StatusUninstalled), + tsRelease("vocal-dogs", 3, 6000, common.StatusUninstalled), } -func tsRelease(name string, vers int, dur time.Duration, status rspb.Status) *rspb.Release { +func tsRelease(name string, vers int, dur time.Duration, status common.Status) *rspb.Release { info := &rspb.Info{Status: status, LastDeployed: time.Now().Add(dur)} return &rspb.Release{ Name: name, diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index de097f294..ada148158 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -60,7 +61,7 @@ func (cfgmaps *ConfigMaps) Name() string { // Get fetches the release named by key. The corresponding release is returned // or error if not found. -func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { +func (cfgmaps *ConfigMaps) Get(key string) (release.Releaser, error) { // fetch the configmap holding the release named by key obj, err := cfgmaps.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { @@ -85,7 +86,7 @@ func (cfgmaps *ConfigMaps) Get(key string) (*rspb.Release, error) { // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // configmap fails to retrieve the releases. -func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (cfgmaps *ConfigMaps) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} @@ -95,7 +96,7 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas return nil, err } - var results []*rspb.Release + var results []release.Releaser // iterate over the configmaps object list // and decode each release @@ -117,7 +118,7 @@ func (cfgmaps *ConfigMaps) List(filter func(*rspb.Release) bool) ([]*rspb.Releas // Query fetches all releases that match the provided map of labels. // An error is returned if the configmap fails to retrieve the releases. -func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, error) { +func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]release.Releaser, error) { ls := kblabels.Set{} for k, v := range labels { if errs := validation.IsValidLabelValue(v); len(errs) != 0 { @@ -138,7 +139,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err return nil, ErrReleaseNotFound } - var results []*rspb.Release + var results []release.Releaser for _, item := range list.Items { rls, err := decodeRelease(item.Data["release"]) if err != nil { @@ -153,18 +154,28 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]*rspb.Release, err // Create creates a new ConfigMap holding the release. If the // ConfigMap already exists, ErrReleaseExists is returned. -func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { +func (cfgmaps *ConfigMaps) Create(key string, rls release.Releaser) error { // set labels for configmaps object meta data var lbs labels + rac, err := release.NewAccessor(rls) + if err != nil { + return err + } + lbs.init() - lbs.fromMap(rls.Labels) + lbs.fromMap(rac.Labels()) lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix())) + rel, err := releaserToV1Release(rls) + if err != nil { + return err + } + // create a new configmap to hold the release - obj, err := newConfigMapsObject(key, rls, lbs) + obj, err := newConfigMapsObject(key, rel, lbs) if err != nil { - slog.Debug("failed to encode release", "name", rls.Name, slog.Any("error", err)) + slog.Debug("failed to encode release", "name", rac.Name(), slog.Any("error", err)) return err } // push the configmap object out into the kubiverse @@ -181,10 +192,15 @@ func (cfgmaps *ConfigMaps) Create(key string, rls *rspb.Release) error { // Update updates the ConfigMap holding the release. If not found // the ConfigMap is created to hold the release. -func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { +func (cfgmaps *ConfigMaps) Update(key string, rel release.Releaser) error { // set labels for configmaps object meta data var lbs labels + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + lbs.init() lbs.fromMap(rls.Labels) lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix())) @@ -205,7 +221,7 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { } // Delete deletes the ConfigMap holding the release named by key. -func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { +func (cfgmaps *ConfigMaps) Delete(key string) (rls release.Releaser, err error) { // fetch the release to check existence if rls, err = cfgmaps.Get(key); err != nil { return nil, err diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index a563eb7d9..8beb45547 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -22,6 +22,8 @@ import ( v1 "k8s.io/api/core/v1" + "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -37,7 +39,7 @@ func TestConfigMapGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) @@ -57,7 +59,7 @@ func TestUncompressedConfigMapGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // Create a test fixture which contains an uncompressed release cfgmap, err := newConfigMapsObject(key, rel, nil) @@ -84,19 +86,35 @@ func TestUncompressedConfigMapGet(t *testing.T) { } } +func convertReleaserToV1(t *testing.T, rel release.Releaser) *rspb.Release { + t.Helper() + switch r := rel.(type) { + case rspb.Release: + return &r + case *rspb.Release: + return r + case nil: + return nil + } + + t.Fatalf("Unsupported release type: %T", rel) + return nil +} + func TestConfigMapList(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) // list all deleted releases - del, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := cfgmaps.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -107,8 +125,9 @@ func TestConfigMapList(t *testing.T) { } // list all deployed releases - dpl, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := cfgmaps.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -119,8 +138,9 @@ func TestConfigMapList(t *testing.T) { } // list all superseded releases - ssd, err := cfgmaps.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := cfgmaps.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -130,7 +150,7 @@ func TestConfigMapList(t *testing.T) { t.Errorf("Expected 2 superseded, got %d", len(ssd)) } // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. - rls := ssd[0] + rls := convertReleaserToV1(t, ssd[0]) _, ok := rls.Labels["name"] if !ok { t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) @@ -143,12 +163,12 @@ func TestConfigMapList(t *testing.T) { func TestConfigMapQuery(t *testing.T) { cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) rls, err := cfgmaps.Query(map[string]string{"status": "deployed"}) @@ -172,7 +192,7 @@ func TestConfigMapCreate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // store the release in a configmap if err := cfgmaps.Create(key, rel); err != nil { @@ -196,12 +216,12 @@ func TestConfigMapUpdate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) // modify release status code - rel.Info.Status = rspb.StatusSuperseded + rel.Info.Status = common.StatusSuperseded // perform the update if err := cfgmaps.Update(key, rel); err != nil { @@ -209,10 +229,11 @@ func TestConfigMapUpdate(t *testing.T) { } // fetch the updated release - got, err := cfgmaps.Get(key) + goti, err := cfgmaps.Get(key) if err != nil { t.Fatalf("Failed to get release with key %q: %s", key, err) } + got := convertReleaserToV1(t, goti) // check release has actually been updated by comparing modified fields if rel.Info.Status != got.Info.Status { @@ -225,7 +246,7 @@ func TestConfigMapDelete(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) diff --git a/pkg/storage/driver/driver.go b/pkg/storage/driver/driver.go index 4f9d63928..6efd1dbaa 100644 --- a/pkg/storage/driver/driver.go +++ b/pkg/storage/driver/driver.go @@ -20,6 +20,7 @@ import ( "errors" "fmt" + "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -58,7 +59,7 @@ func NewErrNoDeployedReleases(releaseName string) error { // Create stores the release or returns ErrReleaseExists // if an identical release already exists. type Creator interface { - Create(key string, rls *rspb.Release) error + Create(key string, rls release.Releaser) error } // Updator is the interface that wraps the Update method. @@ -66,7 +67,7 @@ type Creator interface { // Update updates an existing release or returns // ErrReleaseNotFound if the release does not exist. type Updator interface { - Update(key string, rls *rspb.Release) error + Update(key string, rls release.Releaser) error } // Deletor is the interface that wraps the Delete method. @@ -74,7 +75,7 @@ type Updator interface { // Delete deletes the release named by key or returns // ErrReleaseNotFound if the release does not exist. type Deletor interface { - Delete(key string) (*rspb.Release, error) + Delete(key string) (release.Releaser, error) } // Queryor is the interface that wraps the Get and List methods. @@ -86,9 +87,9 @@ type Deletor interface { // // Query returns the set of all releases that match the provided label set. type Queryor interface { - Get(key string) (*rspb.Release, error) - List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) - Query(labels map[string]string) ([]*rspb.Release, error) + Get(key string) (release.Releaser, error) + List(filter func(release.Releaser) bool) ([]release.Releaser, error) + Query(labels map[string]string) ([]release.Releaser, error) } // Driver is the interface composed of Creator, Updator, Deletor, and Queryor @@ -102,3 +103,18 @@ type Driver interface { Queryor Name() string } + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { + switch r := rel.(type) { + case rspb.Release: + return &r, nil + case *rspb.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } +} diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 79e7f090e..352fe2c6a 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -21,7 +21,7 @@ import ( "strings" "sync" - rspb "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/release" ) var _ Driver = (*Memory)(nil) @@ -61,7 +61,7 @@ func (mem *Memory) Name() string { } // Get returns the release named by key or returns ErrReleaseNotFound. -func (mem *Memory) Get(key string) (*rspb.Release, error) { +func (mem *Memory) Get(key string) (release.Releaser, error) { defer unlock(mem.rlock()) keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") @@ -83,10 +83,10 @@ func (mem *Memory) Get(key string) (*rspb.Release, error) { } // List returns the list of all releases such that filter(release) == true -func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (mem *Memory) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { defer unlock(mem.rlock()) - var ls []*rspb.Release + var ls []release.Releaser for namespace := range mem.cache { if mem.namespace != "" { // Should only list releases of this namespace @@ -109,7 +109,7 @@ func (mem *Memory) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error } // Query returns the set of releases that match the provided set of labels -func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { +func (mem *Memory) Query(keyvals map[string]string) ([]release.Releaser, error) { defer unlock(mem.rlock()) var lbs labels @@ -117,7 +117,7 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { lbs.init() lbs.fromMap(keyvals) - var ls []*rspb.Release + var ls []release.Releaser for namespace := range mem.cache { if mem.namespace != "" { // Should only query releases of this namespace @@ -150,9 +150,13 @@ func (mem *Memory) Query(keyvals map[string]string) ([]*rspb.Release, error) { } // Create creates a new release or returns ErrReleaseExists. -func (mem *Memory) Create(key string, rls *rspb.Release) error { +func (mem *Memory) Create(key string, rel release.Releaser) error { defer unlock(mem.wlock()) + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } // For backwards compatibility, we protect against an unset namespace namespace := rls.Namespace if namespace == "" { @@ -176,9 +180,14 @@ func (mem *Memory) Create(key string, rls *rspb.Release) error { } // Update updates a release or returns ErrReleaseNotFound. -func (mem *Memory) Update(key string, rls *rspb.Release) error { +func (mem *Memory) Update(key string, rel release.Releaser) error { defer unlock(mem.wlock()) + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + // For backwards compatibility, we protect against an unset namespace namespace := rls.Namespace if namespace == "" { @@ -196,7 +205,7 @@ func (mem *Memory) Update(key string, rls *rspb.Release) error { } // Delete deletes a release or returns ErrReleaseNotFound. -func (mem *Memory) Delete(key string) (*rspb.Release, error) { +func (mem *Memory) Delete(key string) (release.Releaser, error) { defer unlock(mem.wlock()) keyWithoutPrefix := strings.TrimPrefix(key, "sh.helm.release.v1.") diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index ee547b58b..18786c004 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -21,6 +21,9 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -38,22 +41,22 @@ func TestMemoryCreate(t *testing.T) { }{ { "create should succeed", - releaseStub("rls-c", 1, "default", rspb.StatusDeployed), + releaseStub("rls-c", 1, "default", common.StatusDeployed), false, }, { "create should fail (release already exists)", - releaseStub("rls-a", 1, "default", rspb.StatusDeployed), + releaseStub("rls-a", 1, "default", common.StatusDeployed), true, }, { "create in namespace should succeed", - releaseStub("rls-a", 1, "mynamespace", rspb.StatusDeployed), + releaseStub("rls-a", 1, "mynamespace", common.StatusDeployed), false, }, { "create in other namespace should fail (release already exists)", - releaseStub("rls-c", 1, "mynamespace", rspb.StatusDeployed), + releaseStub("rls-c", 1, "mynamespace", common.StatusDeployed), true, }, } @@ -104,8 +107,9 @@ func TestMemoryList(t *testing.T) { ts.SetNamespace("default") // list all deployed releases - dpl, err := ts.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := ts.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -116,8 +120,9 @@ func TestMemoryList(t *testing.T) { } // list all superseded releases - ssd, err := ts.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := ts.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -128,8 +133,9 @@ func TestMemoryList(t *testing.T) { } // list all deleted releases - del, err := ts.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := ts.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -185,25 +191,25 @@ func TestMemoryUpdate(t *testing.T) { { "update release status", "rls-a.v4", - releaseStub("rls-a", 4, "default", rspb.StatusSuperseded), + releaseStub("rls-a", 4, "default", common.StatusSuperseded), false, }, { "update release does not exist", "rls-c.v1", - releaseStub("rls-c", 1, "default", rspb.StatusUninstalled), + releaseStub("rls-c", 1, "default", common.StatusUninstalled), true, }, { "update release status in namespace", "rls-c.v4", - releaseStub("rls-c", 4, "mynamespace", rspb.StatusSuperseded), + releaseStub("rls-c", 4, "mynamespace", common.StatusSuperseded), false, }, { "update release in namespace does not exist", "rls-a.v1", - releaseStub("rls-a", 1, "mynamespace", rspb.StatusUninstalled), + releaseStub("rls-a", 1, "mynamespace", common.StatusUninstalled), true, }, } @@ -255,17 +261,23 @@ func TestMemoryDelete(t *testing.T) { startLen := len(start) for _, tt := range tests { ts.SetNamespace(tt.namespace) - if rel, err := ts.Delete(tt.key); err != nil { + + rel, err := ts.Delete(tt.key) + var rls *rspb.Release + if err == nil { + rls = convertReleaserToV1(t, rel) + } + if err != nil { if !tt.err { t.Fatalf("Failed %q to get '%s': %q\n", tt.desc, tt.key, err) } continue } else if tt.err { t.Fatalf("Did not get expected error for %q '%s'\n", tt.desc, tt.key) - } else if fmt.Sprintf("%s.v%d", rel.Name, rel.Version) != tt.key { - t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rel.Version) + } else if fmt.Sprintf("%s.v%d", rls.Name, rls.Version) != tt.key { + t.Fatalf("Asked for delete on %s, but deleted %d", tt.key, rls.Version) } - _, err := ts.Get(tt.key) + _, err = ts.Get(tt.key) if err == nil { t.Errorf("Expected an error when asking for a deleted key") } @@ -282,7 +294,9 @@ func TestMemoryDelete(t *testing.T) { if startLen-2 != endLen { t.Errorf("expected end to be %d instead of %d", startLen-2, endLen) for _, ee := range end { - t.Logf("Name: %s, Version: %d", ee.Name, ee.Version) + rac, err := release.NewAccessor(ee) + assert.NoError(t, err, "unable to get release accessor") + t.Logf("Name: %s, Version: %d", rac.Name(), rac.Version()) } } diff --git a/pkg/storage/driver/mock_test.go b/pkg/storage/driver/mock_test.go index 7dba5fea2..e62b02f43 100644 --- a/pkg/storage/driver/mock_test.go +++ b/pkg/storage/driver/mock_test.go @@ -31,10 +31,11 @@ import ( kblabels "k8s.io/apimachinery/pkg/labels" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) -func releaseStub(name string, vers int, namespace string, status rspb.Status) *rspb.Release { +func releaseStub(name string, vers int, namespace string, status common.Status) *rspb.Release { return &rspb.Release{ Name: name, Version: vers, @@ -55,20 +56,20 @@ func tsFixtureMemory(t *testing.T) *Memory { t.Helper() hs := []*rspb.Release{ // rls-a - releaseStub("rls-a", 4, "default", rspb.StatusDeployed), - releaseStub("rls-a", 1, "default", rspb.StatusSuperseded), - releaseStub("rls-a", 3, "default", rspb.StatusSuperseded), - releaseStub("rls-a", 2, "default", rspb.StatusSuperseded), + releaseStub("rls-a", 4, "default", common.StatusDeployed), + releaseStub("rls-a", 1, "default", common.StatusSuperseded), + releaseStub("rls-a", 3, "default", common.StatusSuperseded), + releaseStub("rls-a", 2, "default", common.StatusSuperseded), // rls-b - releaseStub("rls-b", 4, "default", rspb.StatusDeployed), - releaseStub("rls-b", 1, "default", rspb.StatusSuperseded), - releaseStub("rls-b", 3, "default", rspb.StatusSuperseded), - releaseStub("rls-b", 2, "default", rspb.StatusSuperseded), + releaseStub("rls-b", 4, "default", common.StatusDeployed), + releaseStub("rls-b", 1, "default", common.StatusSuperseded), + releaseStub("rls-b", 3, "default", common.StatusSuperseded), + releaseStub("rls-b", 2, "default", common.StatusSuperseded), // rls-c in other namespace - releaseStub("rls-c", 4, "mynamespace", rspb.StatusDeployed), - releaseStub("rls-c", 1, "mynamespace", rspb.StatusSuperseded), - releaseStub("rls-c", 3, "mynamespace", rspb.StatusSuperseded), - releaseStub("rls-c", 2, "mynamespace", rspb.StatusSuperseded), + releaseStub("rls-c", 4, "mynamespace", common.StatusDeployed), + releaseStub("rls-c", 1, "mynamespace", common.StatusSuperseded), + releaseStub("rls-c", 3, "mynamespace", common.StatusSuperseded), + releaseStub("rls-c", 2, "mynamespace", common.StatusSuperseded), } mem := NewMemory() diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go index 34b2fb80c..24e4ccb4e 100644 --- a/pkg/storage/driver/records_test.go +++ b/pkg/storage/driver/records_test.go @@ -20,13 +20,13 @@ import ( "reflect" "testing" - rspb "helm.sh/helm/v4/pkg/release/v1" + "helm.sh/helm/v4/pkg/release/common" ) func TestRecordsAdd(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -39,13 +39,13 @@ func TestRecordsAdd(t *testing.T) { "add valid key", "rls-a.v3", false, - newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)), }, { "add already existing key", "rls-a.v1", true, - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusDeployed)), }, } @@ -70,8 +70,8 @@ func TestRecordsRemove(t *testing.T) { } rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) startLen := rs.Len() @@ -98,8 +98,8 @@ func TestRecordsRemove(t *testing.T) { func TestRecordsRemoveAt(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) if len(rs) != 2 { @@ -114,8 +114,8 @@ func TestRecordsRemoveAt(t *testing.T) { func TestRecordsGet(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -126,7 +126,7 @@ func TestRecordsGet(t *testing.T) { { "get valid key", "rls-a.v1", - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), }, { "get invalid key", @@ -145,8 +145,8 @@ func TestRecordsGet(t *testing.T) { func TestRecordsIndex(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -176,8 +176,8 @@ func TestRecordsIndex(t *testing.T) { func TestRecordsExists(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -207,8 +207,8 @@ func TestRecordsExists(t *testing.T) { func TestRecordsReplace(t *testing.T) { rs := records([]*record{ - newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }) var tests = []struct { @@ -220,13 +220,13 @@ func TestRecordsReplace(t *testing.T) { { "replace with existing key", "rls-a.v2", - newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), - newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", common.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", common.StatusDeployed)), }, { "replace with non existing key", "rls-a.v4", - newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", rspb.StatusDeployed)), + newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", common.StatusDeployed)), nil, }, } diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 23a8f5cab..1f5ce75ac 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -31,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -60,7 +61,7 @@ func (secrets *Secrets) Name() string { // Get fetches the release named by key. The corresponding release is returned // or error if not found. -func (secrets *Secrets) Get(key string) (*rspb.Release, error) { +func (secrets *Secrets) Get(key string) (release.Releaser, error) { // fetch the secret holding the release named by key obj, err := secrets.impl.Get(context.Background(), key, metav1.GetOptions{}) if err != nil { @@ -81,7 +82,7 @@ func (secrets *Secrets) Get(key string) (*rspb.Release, error) { // List fetches all releases and returns the list releases such // that filter(release) == true. An error is returned if the // secret fails to retrieve the releases. -func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (secrets *Secrets) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { lsel := kblabels.Set{"owner": "helm"}.AsSelector() opts := metav1.ListOptions{LabelSelector: lsel.String()} @@ -90,7 +91,7 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, return nil, fmt.Errorf("list: failed to list: %w", err) } - var results []*rspb.Release + var results []release.Releaser // iterate over the secrets object list // and decode each release @@ -112,7 +113,7 @@ func (secrets *Secrets) List(filter func(*rspb.Release) bool) ([]*rspb.Release, // Query fetches all releases that match the provided map of labels. // An error is returned if the secret fails to retrieve the releases. -func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) { +func (secrets *Secrets) Query(labels map[string]string) ([]release.Releaser, error) { ls := kblabels.Set{} for k, v := range labels { if errs := validation.IsValidLabelValue(v); len(errs) != 0 { @@ -132,7 +133,7 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) return nil, ErrReleaseNotFound } - var results []*rspb.Release + var results []release.Releaser for _, item := range list.Items { rls, err := decodeRelease(string(item.Data["release"])) if err != nil { @@ -147,10 +148,15 @@ func (secrets *Secrets) Query(labels map[string]string) ([]*rspb.Release, error) // Create creates a new Secret holding the release. If the // Secret already exists, ErrReleaseExists is returned. -func (secrets *Secrets) Create(key string, rls *rspb.Release) error { +func (secrets *Secrets) Create(key string, rel release.Releaser) error { // set labels for secrets object meta data var lbs labels + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + lbs.init() lbs.fromMap(rls.Labels) lbs.set("createdAt", fmt.Sprintf("%v", time.Now().Unix())) @@ -173,10 +179,15 @@ func (secrets *Secrets) Create(key string, rls *rspb.Release) error { // Update updates the Secret holding the release. If not found // the Secret is created to hold the release. -func (secrets *Secrets) Update(key string, rls *rspb.Release) error { +func (secrets *Secrets) Update(key string, rel release.Releaser) error { // set labels for secrets object meta data var lbs labels + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + lbs.init() lbs.fromMap(rls.Labels) lbs.set("modifiedAt", fmt.Sprintf("%v", time.Now().Unix())) @@ -195,7 +206,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { } // Delete deletes the Secret holding the release named by key. -func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) { +func (secrets *Secrets) Delete(key string) (rls release.Releaser, err error) { // fetch the release to check existence if rls, err = secrets.Get(key); err != nil { return nil, err diff --git a/pkg/storage/driver/secrets_test.go b/pkg/storage/driver/secrets_test.go index 9e45bae67..f4aa1176c 100644 --- a/pkg/storage/driver/secrets_test.go +++ b/pkg/storage/driver/secrets_test.go @@ -22,6 +22,8 @@ import ( v1 "k8s.io/api/core/v1" + "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -37,7 +39,7 @@ func TestSecretGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) @@ -57,7 +59,7 @@ func TestUNcompressedSecretGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // Create a test fixture which contains an uncompressed release secret, err := newSecretsObject(key, rel, nil) @@ -86,17 +88,18 @@ func TestUNcompressedSecretGet(t *testing.T) { func TestSecretList(t *testing.T) { secrets := newTestFixtureSecrets(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) // list all deleted releases - del, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := secrets.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -107,8 +110,9 @@ func TestSecretList(t *testing.T) { } // list all deployed releases - dpl, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := secrets.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -119,8 +123,9 @@ func TestSecretList(t *testing.T) { } // list all superseded releases - ssd, err := secrets.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := secrets.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -130,7 +135,7 @@ func TestSecretList(t *testing.T) { t.Errorf("Expected 2 superseded, got %d", len(ssd)) } // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. - rls := ssd[0] + rls := convertReleaserToV1(t, ssd[0]) _, ok := rls.Labels["name"] if !ok { t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) @@ -143,12 +148,12 @@ func TestSecretList(t *testing.T) { func TestSecretQuery(t *testing.T) { secrets := newTestFixtureSecrets(t, []*rspb.Release{ - releaseStub("key-1", 1, "default", rspb.StatusUninstalled), - releaseStub("key-2", 1, "default", rspb.StatusUninstalled), - releaseStub("key-3", 1, "default", rspb.StatusDeployed), - releaseStub("key-4", 1, "default", rspb.StatusDeployed), - releaseStub("key-5", 1, "default", rspb.StatusSuperseded), - releaseStub("key-6", 1, "default", rspb.StatusSuperseded), + releaseStub("key-1", 1, "default", common.StatusUninstalled), + releaseStub("key-2", 1, "default", common.StatusUninstalled), + releaseStub("key-3", 1, "default", common.StatusDeployed), + releaseStub("key-4", 1, "default", common.StatusDeployed), + releaseStub("key-5", 1, "default", common.StatusSuperseded), + releaseStub("key-6", 1, "default", common.StatusSuperseded), }...) rls, err := secrets.Query(map[string]string{"status": "deployed"}) @@ -172,7 +177,7 @@ func TestSecretCreate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) // store the release in a secret if err := secrets.Create(key, rel); err != nil { @@ -196,12 +201,12 @@ func TestSecretUpdate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) // modify release status code - rel.Info.Status = rspb.StatusSuperseded + rel.Info.Status = common.StatusSuperseded // perform the update if err := secrets.Update(key, rel); err != nil { @@ -209,10 +214,11 @@ func TestSecretUpdate(t *testing.T) { } // fetch the updated release - got, err := secrets.Get(key) + goti, err := secrets.Get(key) if err != nil { t.Fatalf("Failed to get release with key %q: %s", key, err) } + got := convertReleaserToV1(t, goti) // check release has actually been updated by comparing modified fields if rel.Info.Status != got.Info.Status { @@ -225,7 +231,7 @@ func TestSecretDelete(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index 46f6c6b2e..0020d2436 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -32,6 +32,7 @@ import ( // Import pq for postgres dialect _ "github.com/lib/pq" + "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -297,7 +298,7 @@ func NewSQL(connectionString string, namespace string) (*SQL, error) { } // Get returns the release named by key. -func (s *SQL) Get(key string) (*rspb.Release, error) { +func (s *SQL) Get(key string) (release.Releaser, error) { var record SQLReleaseWrapper qb := s.statementBuilder. @@ -333,7 +334,7 @@ func (s *SQL) Get(key string) (*rspb.Release, error) { } // List returns the list of all releases such that filter(release) == true -func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (s *SQL) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { sb := s.statementBuilder. Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName). @@ -356,7 +357,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { return nil, err } - var releases []*rspb.Release + var releases []release.Releaser for _, record := range records { release, err := decodeRelease(record.Body) if err != nil { @@ -379,7 +380,7 @@ func (s *SQL) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { } // Query returns the set of releases that match the provided set of labels. -func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { +func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) { sb := s.statementBuilder. Select(sqlReleaseTableKeyColumn, sqlReleaseTableNamespaceColumn, sqlReleaseTableBodyColumn). From(sqlReleaseTableName) @@ -420,7 +421,7 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { return nil, ErrReleaseNotFound } - var releases []*rspb.Release + var releases []release.Releaser for _, record := range records { release, err := decodeRelease(record.Body) if err != nil { @@ -444,7 +445,12 @@ func (s *SQL) Query(labels map[string]string) ([]*rspb.Release, error) { } // Create creates a new release. -func (s *SQL) Create(key string, rls *rspb.Release) error { +func (s *SQL) Create(key string, rel release.Releaser) error { + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } + namespace := rls.Namespace if namespace == "" { namespace = defaultNamespace @@ -551,7 +557,11 @@ func (s *SQL) Create(key string, rls *rspb.Release) error { } // Update updates a release. -func (s *SQL) Update(key string, rls *rspb.Release) error { +func (s *SQL) Update(key string, rel release.Releaser) error { + rls, err := releaserToV1Release(rel) + if err != nil { + return err + } namespace := rls.Namespace if namespace == "" { namespace = defaultNamespace @@ -590,7 +600,7 @@ func (s *SQL) Update(key string, rls *rspb.Release) error { } // Delete deletes a release or returns ErrReleaseNotFound. -func (s *SQL) Delete(key string) (*rspb.Release, error) { +func (s *SQL) Delete(key string) (release.Releaser, error) { transaction, err := s.db.Beginx() if err != nil { slog.Debug("failed to start SQL transaction", slog.Any("error", err)) diff --git a/pkg/storage/driver/sql_test.go b/pkg/storage/driver/sql_test.go index fe147816c..d85691a6f 100644 --- a/pkg/storage/driver/sql_test.go +++ b/pkg/storage/driver/sql_test.go @@ -24,6 +24,8 @@ import ( sqlmock "github.com/DATA-DOG/go-sqlmock" migrate "github.com/rubenv/sql-migrate" + "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -66,7 +68,7 @@ func TestSQLGet(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) body, _ := encodeRelease(rel) @@ -109,12 +111,12 @@ func TestSQLGet(t *testing.T) { func TestSQLList(t *testing.T) { releases := []*rspb.Release{} - releases = append(releases, releaseStub("key-1", 1, "default", rspb.StatusUninstalled)) - releases = append(releases, releaseStub("key-2", 1, "default", rspb.StatusUninstalled)) - releases = append(releases, releaseStub("key-3", 1, "default", rspb.StatusDeployed)) - releases = append(releases, releaseStub("key-4", 1, "default", rspb.StatusDeployed)) - releases = append(releases, releaseStub("key-5", 1, "default", rspb.StatusSuperseded)) - releases = append(releases, releaseStub("key-6", 1, "default", rspb.StatusSuperseded)) + releases = append(releases, releaseStub("key-1", 1, "default", common.StatusUninstalled)) + releases = append(releases, releaseStub("key-2", 1, "default", common.StatusUninstalled)) + releases = append(releases, releaseStub("key-3", 1, "default", common.StatusDeployed)) + releases = append(releases, releaseStub("key-4", 1, "default", common.StatusDeployed)) + releases = append(releases, releaseStub("key-5", 1, "default", common.StatusSuperseded)) + releases = append(releases, releaseStub("key-6", 1, "default", common.StatusSuperseded)) sqlDriver, mock := newTestFixtureSQL(t) @@ -147,8 +149,9 @@ func TestSQLList(t *testing.T) { } // list all deleted releases - del, err := sqlDriver.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusUninstalled + del, err := sqlDriver.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusUninstalled }) // check if err != nil { @@ -159,8 +162,9 @@ func TestSQLList(t *testing.T) { } // list all deployed releases - dpl, err := sqlDriver.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusDeployed + dpl, err := sqlDriver.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusDeployed }) // check if err != nil { @@ -171,8 +175,9 @@ func TestSQLList(t *testing.T) { } // list all superseded releases - ssd, err := sqlDriver.List(func(rel *rspb.Release) bool { - return rel.Info.Status == rspb.StatusSuperseded + ssd, err := sqlDriver.List(func(rel release.Releaser) bool { + rls := convertReleaserToV1(t, rel) + return rls.Info.Status == common.StatusSuperseded }) // check if err != nil { @@ -187,7 +192,7 @@ func TestSQLList(t *testing.T) { } // Check if release having both system and custom labels, this is needed to ensure that selector filtering would work. - rls := ssd[0] + rls := convertReleaserToV1(t, ssd[0]) _, ok := rls.Labels["name"] if !ok { t.Fatalf("Expected 'name' label in results, actual %v", rls.Labels) @@ -203,7 +208,7 @@ func TestSqlCreate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) sqlDriver, mock := newTestFixtureSQL(t) body, _ := encodeRelease(rel) @@ -260,7 +265,7 @@ func TestSqlCreateAlreadyExists(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) sqlDriver, mock := newTestFixtureSQL(t) body, _ := encodeRelease(rel) @@ -321,7 +326,7 @@ func TestSqlUpdate(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) sqlDriver, mock := newTestFixtureSQL(t) body, _ := encodeRelease(rel) @@ -370,9 +375,9 @@ func TestSqlQuery(t *testing.T) { "owner": sqlReleaseDefaultOwner, } - supersededRelease := releaseStub("smug-pigeon", 1, "default", rspb.StatusSuperseded) + supersededRelease := releaseStub("smug-pigeon", 1, "default", common.StatusSuperseded) supersededReleaseBody, _ := encodeRelease(supersededRelease) - deployedRelease := releaseStub("smug-pigeon", 2, "default", rspb.StatusDeployed) + deployedRelease := releaseStub("smug-pigeon", 2, "default", common.StatusDeployed) deployedReleaseBody, _ := encodeRelease(deployedRelease) // Let's actually start our test @@ -482,7 +487,7 @@ func TestSqlDelete(t *testing.T) { name := "smug-pigeon" namespace := "default" key := testKey(name, vers) - rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + rel := releaseStub(name, vers, namespace, common.StatusDeployed) body, _ := encodeRelease(rel) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index f086309bb..4603a1de6 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -22,6 +22,8 @@ import ( "log/slog" "strings" + "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" relutil "helm.sh/helm/v4/pkg/release/v1/util" "helm.sh/helm/v4/pkg/storage/driver" @@ -47,7 +49,7 @@ type Storage struct { // Get retrieves the release from storage. An error is returned // if the storage driver failed to fetch the release, or the // release identified by the key, version pair does not exist. -func (s *Storage) Get(name string, version int) (*rspb.Release, error) { +func (s *Storage) Get(name string, version int) (release.Releaser, error) { slog.Debug("getting release", "key", makeKey(name, version)) return s.Driver.Get(makeKey(name, version)) } @@ -55,62 +57,99 @@ func (s *Storage) Get(name string, version int) (*rspb.Release, error) { // Create creates a new storage entry holding the release. An // error is returned if the storage driver fails to store the // release, or a release with an identical key already exists. -func (s *Storage) Create(rls *rspb.Release) error { - slog.Debug("creating release", "key", makeKey(rls.Name, rls.Version)) +func (s *Storage) Create(rls release.Releaser) error { + rac, err := release.NewAccessor(rls) + if err != nil { + return err + } + slog.Debug("creating release", "key", makeKey(rac.Name(), rac.Version())) if s.MaxHistory > 0 { // Want to make space for one more release. - if err := s.removeLeastRecent(rls.Name, s.MaxHistory-1); err != nil && + if err := s.removeLeastRecent(rac.Name(), s.MaxHistory-1); err != nil && !errors.Is(err, driver.ErrReleaseNotFound) { return err } } - return s.Driver.Create(makeKey(rls.Name, rls.Version), rls) + return s.Driver.Create(makeKey(rac.Name(), rac.Version()), rls) } // Update updates the release in storage. An error is returned if the // storage backend fails to update the release or if the release // does not exist. -func (s *Storage) Update(rls *rspb.Release) error { - slog.Debug("updating release", "key", makeKey(rls.Name, rls.Version)) - return s.Driver.Update(makeKey(rls.Name, rls.Version), rls) +func (s *Storage) Update(rls release.Releaser) error { + rac, err := release.NewAccessor(rls) + if err != nil { + return err + } + slog.Debug("updating release", "key", makeKey(rac.Name(), rac.Version())) + return s.Driver.Update(makeKey(rac.Name(), rac.Version()), rls) } // Delete deletes the release from storage. An error is returned if // the storage backend fails to delete the release or if the release // does not exist. -func (s *Storage) Delete(name string, version int) (*rspb.Release, error) { +func (s *Storage) Delete(name string, version int) (release.Releaser, error) { slog.Debug("deleting release", "key", makeKey(name, version)) return s.Driver.Delete(makeKey(name, version)) } // ListReleases returns all releases from storage. An error is returned if the // storage backend fails to retrieve the releases. -func (s *Storage) ListReleases() ([]*rspb.Release, error) { +func (s *Storage) ListReleases() ([]release.Releaser, error) { slog.Debug("listing all releases in storage") - return s.List(func(_ *rspb.Release) bool { return true }) + return s.List(func(_ release.Releaser) bool { return true }) +} + +// releaserToV1Release is a helper function to convert a v1 release passed by interface +// into the type object. +func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { + switch r := rel.(type) { + case rspb.Release: + return &r, nil + case *rspb.Release: + return r, nil + case nil: + return nil, nil + default: + return nil, fmt.Errorf("unsupported release type: %T", rel) + } } // ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned // if the storage backend fails to retrieve the releases. -func (s *Storage) ListUninstalled() ([]*rspb.Release, error) { +func (s *Storage) ListUninstalled() ([]release.Releaser, error) { slog.Debug("listing uninstalled releases in storage") - return s.List(func(rls *rspb.Release) bool { - return relutil.StatusFilter(rspb.StatusUninstalled).Check(rls) + return s.List(func(rls release.Releaser) bool { + rel, err := releaserToV1Release(rls) + if err != nil { + // This will only happen if calling code does not pass the proper types. This is + // a problem with the application and not user data. + slog.Error("unable to convert release to typed release", slog.Any("error", err)) + panic(fmt.Sprintf("unable to convert release to typed release: %s", err)) + } + return relutil.StatusFilter(common.StatusUninstalled).Check(rel) }) } // ListDeployed returns all releases with Status == DEPLOYED. An error is returned // if the storage backend fails to retrieve the releases. -func (s *Storage) ListDeployed() ([]*rspb.Release, error) { +func (s *Storage) ListDeployed() ([]release.Releaser, error) { slog.Debug("listing all deployed releases in storage") - return s.List(func(rls *rspb.Release) bool { - return relutil.StatusFilter(rspb.StatusDeployed).Check(rls) + return s.List(func(rls release.Releaser) bool { + rel, err := releaserToV1Release(rls) + if err != nil { + // This will only happen if calling code does not pass the proper types. This is + // a problem with the application and not user data. + slog.Error("unable to convert release to typed release", slog.Any("error", err)) + panic(fmt.Sprintf("unable to convert release to typed release: %s", err)) + } + return relutil.StatusFilter(common.StatusDeployed).Check(rel) }) } // Deployed returns the last deployed release with the provided release name, or // returns driver.NewErrNoDeployedReleases if not found. -func (s *Storage) Deployed(name string) (*rspb.Release, error) { +func (s *Storage) Deployed(name string) (release.Releaser, error) { ls, err := s.DeployedAll(name) if err != nil { return nil, err @@ -120,16 +159,34 @@ func (s *Storage) Deployed(name string) (*rspb.Release, error) { return nil, driver.NewErrNoDeployedReleases(name) } + rls, err := releaseListToV1List(ls) + if err != nil { + return nil, err + } + // If executed concurrently, Helm's database gets corrupted // and multiple releases are DEPLOYED. Take the latest. - relutil.Reverse(ls, relutil.SortByRevision) + relutil.Reverse(rls, relutil.SortByRevision) - return ls[0], nil + return rls[0], nil +} + +func releaseListToV1List(ls []release.Releaser) ([]*rspb.Release, error) { + rls := make([]*rspb.Release, 0, len(ls)) + for _, val := range ls { + rel, err := releaserToV1Release(val) + if err != nil { + return nil, err + } + rls = append(rls, rel) + } + + return rls, nil } // DeployedAll returns all deployed releases with the provided name, or // returns driver.NewErrNoDeployedReleases if not found. -func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { +func (s *Storage) DeployedAll(name string) ([]release.Releaser, error) { slog.Debug("getting deployed releases", "name", name) ls, err := s.Query(map[string]string{ @@ -148,7 +205,7 @@ func (s *Storage) DeployedAll(name string) ([]*rspb.Release, error) { // History returns the revision history for the release with the provided name, or // returns driver.ErrReleaseNotFound if no such release name exists. -func (s *Storage) History(name string) ([]*rspb.Release, error) { +func (s *Storage) History(name string) ([]release.Releaser, error) { slog.Debug("getting release history", "name", name) return s.Query(map[string]string{"name": name, "owner": "helm"}) @@ -170,23 +227,31 @@ func (s *Storage) removeLeastRecent(name string, maximum int) error { if len(h) <= maximum { return nil } + rls, err := releaseListToV1List(h) + if err != nil { + return err + } // We want oldest to newest - relutil.SortByRevision(h) + relutil.SortByRevision(rls) lastDeployed, err := s.Deployed(name) if err != nil && !errors.Is(err, driver.ErrNoDeployedReleases) { return err } - var toDelete []*rspb.Release - for _, rel := range h { + var toDelete []release.Releaser + for _, rel := range rls { // once we have enough releases to delete to reach the maximum, stop - if len(h)-len(toDelete) == maximum { + if len(rls)-len(toDelete) == maximum { break } if lastDeployed != nil { - if rel.Version != lastDeployed.Version { + ldac, err := release.NewAccessor(lastDeployed) + if err != nil { + return err + } + if rel.Version != ldac.Version() { toDelete = append(toDelete, rel) } } else { @@ -198,7 +263,12 @@ func (s *Storage) removeLeastRecent(name string, maximum int) error { // multiple invocations of this function will eventually delete them all. errs := []error{} for _, rel := range toDelete { - err = s.deleteReleaseVersion(name, rel.Version) + rac, err := release.NewAccessor(rel) + if err != nil { + errs = append(errs, err) + continue + } + err = s.deleteReleaseVersion(name, rac.Version()) if err != nil { errs = append(errs, err) } @@ -226,7 +296,7 @@ func (s *Storage) deleteReleaseVersion(name string, version int) error { } // Last fetches the last revision of the named release. -func (s *Storage) Last(name string) (*rspb.Release, error) { +func (s *Storage) Last(name string) (release.Releaser, error) { slog.Debug("getting last revision", "name", name) h, err := s.History(name) if err != nil { @@ -235,9 +305,13 @@ func (s *Storage) Last(name string) (*rspb.Release, error) { if len(h) == 0 { return nil, fmt.Errorf("no revision for release %q", name) } + rls, err := releaseListToV1List(h) + if err != nil { + return nil, err + } - relutil.Reverse(h, relutil.SortByRevision) - return h[0], nil + relutil.Reverse(rls, relutil.SortByRevision) + return rls[0], nil } // makeKey concatenates the Kubernetes storage object type, a release name and version diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index d3025eca3..283bd8199 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -22,6 +22,9 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/release" + "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" "helm.sh/helm/v4/pkg/storage/driver" ) @@ -56,13 +59,13 @@ func TestStorageUpdate(t *testing.T) { rls := ReleaseTestData{ Name: "angry-beaver", Version: 1, - Status: rspb.StatusDeployed, + Status: common.StatusDeployed, }.ToRelease() assertErrNil(t.Fatal, storage.Create(rls), "StoreRelease") // modify the release - rls.Info.Status = rspb.StatusUninstalled + rls.Info.Status = common.StatusUninstalled assertErrNil(t.Fatal, storage.Update(rls), "UpdateRelease") // retrieve the updated release @@ -106,13 +109,16 @@ func TestStorageDelete(t *testing.T) { t.Errorf("unexpected error: %s", err) } + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + // We have now deleted one of the two records. - if len(hist) != 1 { + if len(rhist) != 1 { t.Errorf("expected 1 record for deleted release version, got %d", len(hist)) } - if hist[0].Version != 2 { - t.Errorf("Expected version to be 2, got %d", hist[0].Version) + if rhist[0].Version != 2 { + t.Errorf("Expected version to be 2, got %d", rhist[0].Version) } } @@ -123,13 +129,13 @@ func TestStorageList(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: "happy-catdog", Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: "livid-human", Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: "relaxed-cat", Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: "hungry-hippo", Status: rspb.StatusDeployed}.ToRelease() - rls4 := ReleaseTestData{Name: "angry-beaver", Status: rspb.StatusDeployed}.ToRelease() - rls5 := ReleaseTestData{Name: "opulent-frog", Status: rspb.StatusUninstalled}.ToRelease() - rls6 := ReleaseTestData{Name: "happy-liger", Status: rspb.StatusUninstalled}.ToRelease() + rls0 := ReleaseTestData{Name: "happy-catdog", Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: "livid-human", Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: "relaxed-cat", Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: "hungry-hippo", Status: common.StatusDeployed}.ToRelease() + rls4 := ReleaseTestData{Name: "angry-beaver", Status: common.StatusDeployed}.ToRelease() + rls5 := ReleaseTestData{Name: "opulent-frog", Status: common.StatusUninstalled}.ToRelease() + rls6 := ReleaseTestData{Name: "happy-liger", Status: common.StatusUninstalled}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'rls0'") @@ -144,7 +150,7 @@ func TestStorageList(t *testing.T) { var listTests = []struct { Description string NumExpected int - ListFunc func() ([]*rspb.Release, error) + ListFunc func() ([]release.Releaser, error) }{ {"ListDeployed", 2, storage.ListDeployed}, {"ListReleases", 7, storage.ListReleases}, @@ -175,10 +181,10 @@ func TestStorageDeployed(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -194,15 +200,18 @@ func TestStorageDeployed(t *testing.T) { t.Fatalf("Failed to query for deployed release: %s\n", err) } + rel, err := releaserToV1Release(rls) + assert.NoError(t, err) + switch { case rls == nil: t.Fatalf("Release is nil") - case rls.Name != name: - t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) - case rls.Version != vers: - t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) - case rls.Info.Status != rspb.StatusDeployed: - t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) + case rel.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rel.Name) + case rel.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rel.Version) + case rel.Info.Status != common.StatusDeployed: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rel.Info.Status.String()) } } @@ -215,10 +224,10 @@ func TestStorageDeployedWithCorruption(t *testing.T) { // setup storage with test releases setup := func() { // release records (notice odd order and corruption) - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -234,15 +243,18 @@ func TestStorageDeployedWithCorruption(t *testing.T) { t.Fatalf("Failed to query for deployed release: %s\n", err) } + rel, err := releaserToV1Release(rls) + assert.NoError(t, err) + switch { case rls == nil: t.Fatalf("Release is nil") - case rls.Name != name: - t.Fatalf("Expected release name %q, actual %q\n", name, rls.Name) - case rls.Version != vers: - t.Fatalf("Expected release version %d, actual %d\n", vers, rls.Version) - case rls.Info.Status != rspb.StatusDeployed: - t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rls.Info.Status.String()) + case rel.Name != name: + t.Fatalf("Expected release name %q, actual %q\n", name, rel.Name) + case rel.Version != vers: + t.Fatalf("Expected release version %d, actual %d\n", vers, rel.Version) + case rel.Info.Status != common.StatusDeployed: + t.Fatalf("Expected release status 'DEPLOYED', actual %s\n", rel.Info.Status.String()) } } @@ -254,10 +266,10 @@ func TestStorageHistory(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -286,22 +298,22 @@ type MaxHistoryMockDriver struct { func NewMaxHistoryMockDriver(d driver.Driver) *MaxHistoryMockDriver { return &MaxHistoryMockDriver{Driver: d} } -func (d *MaxHistoryMockDriver) Create(key string, rls *rspb.Release) error { +func (d *MaxHistoryMockDriver) Create(key string, rls release.Releaser) error { return d.Driver.Create(key, rls) } -func (d *MaxHistoryMockDriver) Update(key string, rls *rspb.Release) error { +func (d *MaxHistoryMockDriver) Update(key string, rls release.Releaser) error { return d.Driver.Update(key, rls) } -func (d *MaxHistoryMockDriver) Delete(_ string) (*rspb.Release, error) { +func (d *MaxHistoryMockDriver) Delete(_ string) (release.Releaser, error) { return nil, errMaxHistoryMockDriverSomethingHappened } -func (d *MaxHistoryMockDriver) Get(key string) (*rspb.Release, error) { +func (d *MaxHistoryMockDriver) Get(key string) (release.Releaser, error) { return d.Driver.Get(key) } -func (d *MaxHistoryMockDriver) List(filter func(*rspb.Release) bool) ([]*rspb.Release, error) { +func (d *MaxHistoryMockDriver) List(filter func(release.Releaser) bool) ([]release.Releaser, error) { return d.Driver.List(filter) } -func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]*rspb.Release, error) { +func (d *MaxHistoryMockDriver) Query(labels map[string]string) ([]release.Releaser, error) { return d.Driver.Query(labels) } func (d *MaxHistoryMockDriver) Name() string { @@ -319,14 +331,14 @@ func TestMaxHistoryErrorHandling(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls1 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Driver.Create(makeKey(rls1.Name, rls1.Version), rls1), "Storing release 'angry-bird' (v1)") } setup() - rls2 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() wantErr := errMaxHistoryMockDriverSomethingHappened gotErr := storage.Create(rls2) if !errors.Is(gotErr, wantErr) { @@ -345,10 +357,10 @@ func TestStorageRemoveLeastRecent(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusDeployed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusDeployed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -367,22 +379,24 @@ func TestStorageRemoveLeastRecent(t *testing.T) { } storage.MaxHistory = 3 - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusDeployed}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusDeployed}.ToRelease() assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") // On inserting the 5th record, we expect two records to be pruned from history. hist, err := storage.History(name) + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) if err != nil { t.Fatal(err) - } else if len(hist) != storage.MaxHistory { - for _, item := range hist { + } else if len(rhist) != storage.MaxHistory { + for _, item := range rhist { t.Logf("%s %v", item.Name, item.Version) } - t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) + t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(rhist)) } // We expect the existing records to be 3, 4, and 5. - for i, item := range hist { + for i, item := range rhist { v := item.Version if expect := i + 3; v != expect { t.Errorf("Expected release %d, got %d", expect, v) @@ -399,10 +413,10 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusDeployed}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusDeployed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusFailed}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -412,7 +426,7 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { } setup() - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusFailed}.ToRelease() assertErrNil(t.Fatal, storage.Create(rls5), "Storing release 'angry-bird' (v5)") // On inserting the 5th record, we expect a total of 3 releases, but we expect version 2 @@ -421,10 +435,12 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { if err != nil { t.Fatal(err) } else if len(hist) != storage.MaxHistory { - for _, item := range hist { + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + for _, item := range rhist { t.Logf("%s %v", item.Name, item.Version) } - t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(hist)) + t.Fatalf("expected %d items in history, got %d", storage.MaxHistory, len(rhist)) } expectedVersions := map[int]bool{ @@ -433,7 +449,9 @@ func TestStorageDoNotDeleteDeployed(t *testing.T) { 5: true, } - for _, item := range hist { + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + for _, item := range rhist { if !expectedVersions[item.Version] { t.Errorf("Release version %d, found when not expected", item.Version) } @@ -448,10 +466,10 @@ func TestStorageLast(t *testing.T) { // Set up storage with test releases. setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusSuperseded}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusSuperseded}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusSuperseded}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusSuperseded}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusSuperseded}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusSuperseded}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -467,8 +485,11 @@ func TestStorageLast(t *testing.T) { t.Fatalf("Failed to query for release history (%q): %s\n", name, err) } - if h.Version != 4 { - t.Errorf("Expected revision 4, got %d", h.Version) + rel, err := releaserToV1Release(h) + assert.NoError(t, err) + + if rel.Version != 4 { + t.Errorf("Expected revision 4, got %d", rel.Version) } } @@ -483,10 +504,10 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { // setup storage with test releases setup := func() { // release records - rls0 := ReleaseTestData{Name: name, Version: 1, Status: rspb.StatusFailed}.ToRelease() - rls1 := ReleaseTestData{Name: name, Version: 2, Status: rspb.StatusFailed}.ToRelease() - rls2 := ReleaseTestData{Name: name, Version: 3, Status: rspb.StatusFailed}.ToRelease() - rls3 := ReleaseTestData{Name: name, Version: 4, Status: rspb.StatusFailed}.ToRelease() + rls0 := ReleaseTestData{Name: name, Version: 1, Status: common.StatusFailed}.ToRelease() + rls1 := ReleaseTestData{Name: name, Version: 2, Status: common.StatusFailed}.ToRelease() + rls2 := ReleaseTestData{Name: name, Version: 3, Status: common.StatusFailed}.ToRelease() + rls3 := ReleaseTestData{Name: name, Version: 4, Status: common.StatusFailed}.ToRelease() // create the release records in the storage assertErrNil(t.Fatal, storage.Create(rls0), "Storing release 'angry-bird' (v1)") @@ -507,7 +528,7 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { setup() - rls5 := ReleaseTestData{Name: name, Version: 5, Status: rspb.StatusFailed}.ToRelease() + rls5 := ReleaseTestData{Name: name, Version: 5, Status: common.StatusFailed}.ToRelease() err := storage.Create(rls5) if err != nil { t.Fatalf("Failed to create a new release version: %s", err) @@ -518,13 +539,15 @@ func TestUpgradeInitiallyFailedReleaseWithHistoryLimit(t *testing.T) { t.Fatalf("unexpected error: %s", err) } - for i, rel := range hist { + rhist, err := releaseListToV1List(hist) + assert.NoError(t, err) + for i, rel := range rhist { wantVersion := i + 2 if rel.Version != wantVersion { t.Fatalf("Expected history release %d version to equal %d, got %d", i+1, wantVersion, rel.Version) } - wantStatus := rspb.StatusFailed + wantStatus := common.StatusFailed if rel.Info.Status != wantStatus { t.Fatalf("Expected history release %d status to equal %q, got %q", i+1, wantStatus, rel.Info.Status) } @@ -536,7 +559,7 @@ type ReleaseTestData struct { Version int Manifest string Namespace string - Status rspb.Status + Status common.Status } func (test ReleaseTestData) ToRelease() *rspb.Release { From 9c958dec766b485ba8a5f60513843cf7105855dd Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 8 Oct 2025 18:22:04 -0400 Subject: [PATCH 076/118] Making uninstall response generic Signed-off-by: Matt Farina --- pkg/action/uninstall.go | 11 ++++++----- pkg/action/uninstall_test.go | 6 ++++-- pkg/release/{v1 => }/responses.go | 4 ++-- pkg/release/v1/info.go | 1 + pkg/storage/driver/memory_test.go | 1 + pkg/storage/storage_test.go | 2 ++ 6 files changed, 16 insertions(+), 9 deletions(-) rename pkg/release/{v1 => }/responses.go (92%) diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 669431741..6cfde9a58 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -27,6 +27,7 @@ import ( chartutil "helm.sh/helm/v4/pkg/chart/v2/util" "helm.sh/helm/v4/pkg/kube" + releasei "helm.sh/helm/v4/pkg/release" "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" @@ -57,7 +58,7 @@ func NewUninstall(cfg *Configuration) *Uninstall { } // Run uninstalls the given release. -func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) { +func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) { if err := u.cfg.KubeClient.IsReachable(); err != nil { return nil, err } @@ -74,13 +75,13 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) if u.IgnoreNotFound && errors.Is(err, driver.ErrReleaseNotFound) { return nil, nil } - return &release.UninstallReleaseResponse{}, err + return &releasei.UninstallReleaseResponse{}, err } r, err := releaserToV1Release(ri) if err != nil { return nil, err } - return &release.UninstallReleaseResponse{Release: r}, nil + return &releasei.UninstallReleaseResponse{Release: r}, nil } if err := chartutil.ValidateReleaseName(name); err != nil { @@ -113,7 +114,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) if err := u.purgeReleases(rels...); err != nil { return nil, fmt.Errorf("uninstall: Failed to purge the release: %w", err) } - return &release.UninstallReleaseResponse{Release: rel}, nil + return &releasei.UninstallReleaseResponse{Release: rel}, nil } return nil, fmt.Errorf("the release named %q is already deleted", name) } @@ -122,7 +123,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error) rel.Info.Status = common.StatusUninstalling rel.Info.Deleted = time.Now() rel.Info.Description = "Deletion in progress (or silently failed)" - res := &release.UninstallReleaseResponse{Release: rel} + res := &releasei.UninstallReleaseResponse{Release: rel} if !u.DisableHooks { serverSideApply := true diff --git a/pkg/action/uninstall_test.go b/pkg/action/uninstall_test.go index 667e96820..62baec139 100644 --- a/pkg/action/uninstall_test.go +++ b/pkg/action/uninstall_test.go @@ -116,10 +116,12 @@ func TestUninstallRelease_Wait(t *testing.T) { failer := unAction.cfg.KubeClient.(*kubefake.FailingKubeClient) failer.WaitForDeleteError = fmt.Errorf("U timed out") unAction.cfg.KubeClient = failer - res, err := unAction.Run(rel.Name) + resi, err := unAction.Run(rel.Name) is.Error(err) is.Contains(err.Error(), "U timed out") - is.Equal(res.Release.Info.Status, common.StatusUninstalled) + res, err := releaserToV1Release(resi.Release) + is.NoError(err) + is.Equal(res.Info.Status, common.StatusUninstalled) } func TestUninstallRelease_Cascade(t *testing.T) { diff --git a/pkg/release/v1/responses.go b/pkg/release/responses.go similarity index 92% rename from pkg/release/v1/responses.go rename to pkg/release/responses.go index 2a5608c67..6e0a0eaec 100644 --- a/pkg/release/v1/responses.go +++ b/pkg/release/responses.go @@ -13,12 +13,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -package v1 +package release // UninstallReleaseResponse represents a successful response to an uninstall request. type UninstallReleaseResponse struct { // Release is the release that was marked deleted. - Release *Release `json:"release,omitempty"` + Release Releaser `json:"release,omitempty"` // Info is an uninstall message Info string `json:"info,omitempty"` } diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index d5b5089dc..f502b7bc9 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -19,6 +19,7 @@ import ( "time" "helm.sh/helm/v4/pkg/release/common" + "k8s.io/apimachinery/pkg/runtime" ) diff --git a/pkg/storage/driver/memory_test.go b/pkg/storage/driver/memory_test.go index 18786c004..329b82b2f 100644 --- a/pkg/storage/driver/memory_test.go +++ b/pkg/storage/driver/memory_test.go @@ -22,6 +22,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/release" "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" diff --git a/pkg/storage/storage_test.go b/pkg/storage/storage_test.go index 283bd8199..5b2a3bba5 100644 --- a/pkg/storage/storage_test.go +++ b/pkg/storage/storage_test.go @@ -23,6 +23,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "helm.sh/helm/v4/pkg/release" "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" @@ -384,6 +385,7 @@ func TestStorageRemoveLeastRecent(t *testing.T) { // On inserting the 5th record, we expect two records to be pruned from history. hist, err := storage.History(name) + assert.NoError(t, err) rhist, err := releaseListToV1List(hist) assert.NoError(t, err) if err != nil { From a229bf6529fb3a5973210b8d82b3c1e6273a9100 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 8 Oct 2025 18:45:24 -0400 Subject: [PATCH 077/118] Ensure the release interface is used in actions instead of the type Signed-off-by: Matt Farina --- pkg/action/hooks_test.go | 8 +++- pkg/action/install.go | 4 +- pkg/action/install_test.go | 80 ++++++++++++++++++++++++++++---------- pkg/action/status.go | 4 +- pkg/action/upgrade.go | 5 ++- pkg/action/upgrade_test.go | 53 ++++++++++++++++++------- pkg/cmd/install.go | 7 +++- pkg/cmd/status.go | 6 ++- 8 files changed, 123 insertions(+), 44 deletions(-) diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index f43c45d77..c885ce603 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -185,7 +185,9 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str } vals := map[string]interface{}{} - res, err := instAction.Run(buildChartWithTemplates(templates), vals) + resi, err := instAction.Run(buildChartWithTemplates(templates), vals) + is.NoError(err) + res, err := releaserToV1Release(resi) is.NoError(err) is.Equal(expectedOutput, outBuffer.String()) is.Equal(rcommon.StatusDeployed, res.Info.Status) @@ -212,8 +214,10 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str } vals := map[string]interface{}{} - res, err := instAction.Run(buildChartWithTemplates(templates), vals) + resi, err := instAction.Run(buildChartWithTemplates(templates), vals) is.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "failed pre-install") is.Equal(expectedOutput, outBuffer.String()) is.Equal(rcommon.StatusFailed, res.Info.Status) diff --git a/pkg/action/install.go b/pkg/action/install.go index 49a36b276..e4b23aceb 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -245,7 +245,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error { // // If DryRun is set to true, this will prepare the release, but not install it -func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (*release.Release, error) { +func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (ri.Releaser, error) { ctx := context.Background() return i.RunWithContext(ctx, chrt, vals) } @@ -254,7 +254,7 @@ func (i *Install) Run(chrt ci.Charter, vals map[string]interface{}) (*release.Re // // When the task is cancelled through ctx, the function returns and the install // proceeds in the background. -func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (*release.Release, error) { +func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[string]interface{}) (ri.Releaser, error) { var chrt *chart.Chart switch c := ch.(type) { case *chart.Chart: diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 535b14559..3900c0633 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -131,10 +131,12 @@ func TestInstallRelease(t *testing.T) { instAction := installAction(t) vals := map[string]interface{}{} ctx, done := context.WithCancel(t.Context()) - res, err := instAction.RunWithContext(ctx, buildChart(), vals) + resi, err := instAction.RunWithContext(ctx, buildChart(), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Equal(res.Name, "test-install-release", "Expected release name.") is.Equal(res.Namespace, "spaced") @@ -181,10 +183,12 @@ func TestInstallReleaseWithTakeOwnership_ResourceNotOwned(t *testing.T) { config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(false)) instAction := installActionWithConfig(config) instAction.TakeOwnership = true - res, err := instAction.Run(buildChart(), nil) + resi, err := instAction.Run(buildChart(), nil) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) r, err := instAction.cfg.Releases.Get(res.Name, res.Version) is.NoError(err) @@ -202,10 +206,12 @@ func TestInstallReleaseWithTakeOwnership_ResourceOwned(t *testing.T) { config := actionConfigFixtureWithDummyResources(t, createDummyResourceList(true)) instAction := installActionWithConfig(config) instAction.TakeOwnership = false - res, err := instAction.Run(buildChart(), nil) + resi, err := instAction.Run(buildChart(), nil) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) r, err := instAction.cfg.Releases.Get(res.Name, res.Version) is.NoError(err) @@ -239,10 +245,12 @@ func TestInstallReleaseWithValues(t *testing.T) { "simpleKey": "simpleValue", }, } - res, err := instAction.Run(buildChart(withSampleValues()), userVals) + resi, err := instAction.Run(buildChart(withSampleValues()), userVals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Equal(res.Name, "test-install-release", "Expected release name.") is.Equal(res.Namespace, "spaced") @@ -280,10 +288,12 @@ func TestInstallRelease_WithNotes(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "with-notes" vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("note here")), vals) + resi, err := instAction.Run(buildChart(withNotes("note here")), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Equal(res.Name, "with-notes") is.Equal(res.Namespace, "spaced") @@ -309,10 +319,12 @@ func TestInstallRelease_WithNotesRendered(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "with-notes" vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals) + resi, err := instAction.Run(buildChart(withNotes("got-{{.Release.Name}}")), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) r, err := instAction.cfg.Releases.Get(res.Name, res.Version) is.NoError(err) @@ -330,10 +342,12 @@ func TestInstallRelease_WithChartAndDependencyParentNotes(t *testing.T) { instAction := installAction(t) instAction.ReleaseName = "with-notes" vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) + resi, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) r, err := instAction.cfg.Releases.Get(res.Name, res.Version) is.NoError(err) @@ -351,10 +365,12 @@ func TestInstallRelease_WithChartAndDependencyAllNotes(t *testing.T) { instAction.ReleaseName = "with-notes" instAction.SubNotes = true vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) + resi, err := instAction.Run(buildChart(withNotes("parent"), withDependency(withNotes("child"))), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) r, err := instAction.cfg.Releases.Get(res.Name, res.Version) is.NoError(err) @@ -375,10 +391,12 @@ func TestInstallRelease_DryRunClient(t *testing.T) { instAction.DryRunStrategy = dryRunStrategy vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withSampleTemplates()), vals) + resi, err := instAction.Run(buildChart(withSampleTemplates()), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Manifest, "---\n# Source: hello/templates/hello\nhello: world") is.Contains(res.Manifest, "---\n# Source: hello/templates/goodbye\ngoodbye: world") @@ -401,10 +419,12 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { // First perform a normal dry-run with the secret and confirm its presence. instAction.DryRunStrategy = DryRunClient vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) + resi, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret") _, err = instAction.cfg.Releases.Get(res.Name, res.Version) @@ -414,10 +434,12 @@ func TestInstallRelease_DryRunHiddenSecret(t *testing.T) { // Perform a dry-run where the secret should not be present instAction.HideSecret = true vals = map[string]interface{}{} - res2, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) + res2i, err := instAction.Run(buildChart(withSampleSecret(), withSampleTemplates()), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res2, err := releaserToV1Release(res2i) + is.NoError(err) is.NotContains(res2.Manifest, "---\n# Source: hello/templates/secret.yaml\napiVersion: v1\nkind: Secret") @@ -447,10 +469,12 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) { Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), }) - res, err := instAction.Run(mockChart, vals) + resi, err := instAction.Run(mockChart, vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Manifest, "goodbye: map[]") } @@ -478,10 +502,12 @@ func TestInstallRelease_NoHooks(t *testing.T) { instAction.cfg.Releases.Create(releaseStub()) vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) + resi, err := instAction.Run(buildChart(), vals) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.True(res.Hooks[0].LastRun.CompletedAt.IsZero(), "hooks should not run with no-hooks") } @@ -497,8 +523,10 @@ func TestInstallRelease_FailedHooks(t *testing.T) { failer.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) + resi, err := instAction.Run(buildChart(), vals) is.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "failed post-install") is.Equal("", outBuffer.String()) is.Equal(rcommon.StatusFailed, res.Info.Status) @@ -515,7 +543,9 @@ func TestInstallRelease_ReplaceRelease(t *testing.T) { instAction.ReleaseName = rel.Name vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) + resi, err := instAction.Run(buildChart(), vals) + is.NoError(err) + res, err := releaserToV1Release(resi) is.NoError(err) // This should have been auto-incremented @@ -556,8 +586,10 @@ func TestInstallRelease_Wait(t *testing.T) { goroutines := instAction.getGoroutineCount() - res, err := instAction.Run(buildChart(), vals) + resi, err := instAction.Run(buildChart(), vals) is.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, rcommon.StatusFailed) @@ -597,8 +629,10 @@ func TestInstallRelease_WaitForJobs(t *testing.T) { instAction.WaitForJobs = true vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) + resi, err := instAction.Run(buildChart(), vals) is.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, rcommon.StatusFailed) } @@ -618,11 +652,13 @@ func TestInstallRelease_RollbackOnFailure(t *testing.T) { instAction.DisableHooks = true vals := map[string]interface{}{} - res, err := instAction.Run(buildChart(), vals) + resi, err := instAction.Run(buildChart(), vals) is.Error(err) is.Contains(err.Error(), "I timed out") is.Contains(err.Error(), "rollback-on-failure") + res, err := releaserToV1Release(resi) + is.NoError(err) // Now make sure it isn't in storage anymore _, err = instAction.cfg.Releases.Get(res.Name, res.Version) is.Error(err) @@ -662,12 +698,14 @@ func TestInstallRelease_RollbackOnFailure_Interrupted(t *testing.T) { goroutines := instAction.getGoroutineCount() - res, err := instAction.RunWithContext(ctx, buildChart(), vals) + resi, err := instAction.RunWithContext(ctx, buildChart(), vals) is.Error(err) is.Contains(err.Error(), "context canceled") is.Contains(err.Error(), "rollback-on-failure") is.Contains(err.Error(), "uninstalled") + res, err := releaserToV1Release(resi) + is.NoError(err) // Now make sure it isn't in storage anymore _, err = instAction.cfg.Releases.Get(res.Name, res.Version) is.Error(err) @@ -924,10 +962,12 @@ func TestInstallWithLabels(t *testing.T) { "key1": "val1", "key2": "val2", } - res, err := instAction.Run(buildChart(), nil) + resi, err := instAction.Run(buildChart(), nil) if err != nil { t.Fatalf("Failed install: %s", err) } + res, err := releaserToV1Release(resi) + is.NoError(err) is.Equal(instAction.Labels, res.Labels) } diff --git a/pkg/action/status.go b/pkg/action/status.go index a9f2dcd4f..967500db8 100644 --- a/pkg/action/status.go +++ b/pkg/action/status.go @@ -21,7 +21,7 @@ import ( "errors" "helm.sh/helm/v4/pkg/kube" - release "helm.sh/helm/v4/pkg/release/v1" + ri "helm.sh/helm/v4/pkg/release" ) // Status is the action for checking the deployment status of releases. @@ -45,7 +45,7 @@ func NewStatus(cfg *Configuration) *Status { } // Run executes 'helm status' against the given release. -func (s *Status) Run(name string) (*release.Release, error) { +func (s *Status) Run(name string) (ri.Releaser, error) { if err := s.cfg.KubeClient.IsReachable(); err != nil { return nil, err } diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index dc3761761..7ae15f5a2 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -36,6 +36,7 @@ import ( "helm.sh/helm/v4/pkg/kube" "helm.sh/helm/v4/pkg/postrenderer" "helm.sh/helm/v4/pkg/registry" + ri "helm.sh/helm/v4/pkg/release" rcommon "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" releaseutil "helm.sh/helm/v4/pkg/release/v1/util" @@ -152,13 +153,13 @@ func (u *Upgrade) SetRegistryClient(client *registry.Client) { } // Run executes the upgrade on the given release. -func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]interface{}) (*release.Release, error) { +func (u *Upgrade) Run(name string, chart chart.Charter, vals map[string]interface{}) (ri.Releaser, error) { ctx := context.Background() return u.RunWithContext(ctx, name, chart, vals) } // RunWithContext executes the upgrade on the given release with context. -func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Charter, vals map[string]interface{}) (*release.Release, error) { +func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Charter, vals map[string]interface{}) (ri.Releaser, error) { if err := u.cfg.KubeClient.IsReachable(); err != nil { return nil, err } diff --git a/pkg/action/upgrade_test.go b/pkg/action/upgrade_test.go index 67320b207..e1eac3f9f 100644 --- a/pkg/action/upgrade_test.go +++ b/pkg/action/upgrade_test.go @@ -60,8 +60,10 @@ func TestUpgradeRelease_Success(t *testing.T) { vals := map[string]interface{}{} ctx, done := context.WithCancel(t.Context()) - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) req.NoError(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Equal(res.Info.Status, common.StatusDeployed) done() @@ -91,8 +93,10 @@ func TestUpgradeRelease_Wait(t *testing.T) { upAction.WaitStrategy = kube.StatusWatcherStrategy vals := map[string]interface{}{} - res, err := upAction.Run(rel.Name, buildChart(), vals) + resi, err := upAction.Run(rel.Name, buildChart(), vals) req.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, common.StatusFailed) } @@ -114,8 +118,10 @@ func TestUpgradeRelease_WaitForJobs(t *testing.T) { upAction.WaitForJobs = true vals := map[string]interface{}{} - res, err := upAction.Run(rel.Name, buildChart(), vals) + resi, err := upAction.Run(rel.Name, buildChart(), vals) req.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, common.StatusFailed) } @@ -138,9 +144,11 @@ func TestUpgradeRelease_CleanupOnFail(t *testing.T) { upAction.CleanupOnFail = true vals := map[string]interface{}{} - res, err := upAction.Run(rel.Name, buildChart(), vals) + resi, err := upAction.Run(rel.Name, buildChart(), vals) req.Error(err) is.NotContains(err.Error(), "unable to cleanup resources") + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "I timed out") is.Equal(res.Info.Status, common.StatusFailed) } @@ -164,10 +172,12 @@ func TestUpgradeRelease_RollbackOnFailure(t *testing.T) { upAction.RollbackOnFailure = true vals := map[string]interface{}{} - res, err := upAction.Run(rel.Name, buildChart(), vals) + resi, err := upAction.Run(rel.Name, buildChart(), vals) req.Error(err) is.Contains(err.Error(), "arming key removed") is.Contains(err.Error(), "rollback-on-failure") + res, err := releaserToV1Release(resi) + is.NoError(err) // Now make sure it is actually upgraded updatedResi, err := upAction.cfg.Releases.Get(res.Name, 3) @@ -231,7 +241,9 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { upAction.ReuseValues = true // setting newValues and upgrading - res, err := upAction.Run(rel.Name, buildChart(), newValues) + resi, err := upAction.Run(rel.Name, buildChart(), newValues) + is.NoError(err) + res, err := releaserToV1Release(resi) is.NoError(err) // Now make sure it is actually upgraded @@ -296,7 +308,9 @@ func TestUpgradeRelease_ReuseValues(t *testing.T) { withMetadataDependency(dependency), ) // reusing values and upgrading - res, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{}) + resi, err := upAction.Run(rel.Name, sampleChartWithSubChart, map[string]interface{}{}) + is.NoError(err) + res, err := releaserToV1Release(resi) is.NoError(err) // Now get the upgraded release @@ -358,7 +372,9 @@ func TestUpgradeRelease_ResetThenReuseValues(t *testing.T) { upAction.ResetThenReuseValues = true // setting newValues and upgrading - res, err := upAction.Run(rel.Name, buildChart(withValues(newChartValues)), newValues) + resi, err := upAction.Run(rel.Name, buildChart(withValues(newChartValues)), newValues) + is.NoError(err) + res, err := releaserToV1Release(resi) is.NoError(err) // Now make sure it is actually upgraded @@ -417,9 +433,11 @@ func TestUpgradeRelease_Interrupted_Wait(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(time.Second, cancel) - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) req.Error(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Contains(res.Info.Description, "Upgrade \"interrupted-release\" failed: context canceled") is.Equal(res.Info.Status, common.StatusFailed) } @@ -444,11 +462,12 @@ func TestUpgradeRelease_Interrupted_RollbackOnFailure(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) time.AfterFunc(time.Second, cancel) - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) + resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(), vals) req.Error(err) is.Contains(err.Error(), "release interrupted-release failed, and has been rolled back due to rollback-on-failure being set: context canceled") - + res, err := releaserToV1Release(resi) + is.NoError(err) // Now make sure it is actually upgraded updatedResi, err := upAction.cfg.Releases.Get(res.Name, 3) is.NoError(err) @@ -495,7 +514,9 @@ func TestUpgradeRelease_Labels(t *testing.T) { "key3": "val3", } // setting newValues and upgrading - res, err := upAction.Run(rel.Name, buildChart(), nil) + resi, err := upAction.Run(rel.Name, buildChart(), nil) + is.NoError(err) + res, err := releaserToV1Release(resi) is.NoError(err) // Now make sure it is actually upgraded and labels were merged @@ -569,9 +590,11 @@ func TestUpgradeRelease_DryRun(t *testing.T) { vals := map[string]interface{}{} ctx, done := context.WithCancel(t.Context()) - res, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) + resi, err := upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) done() req.NoError(err) + res, err := releaserToV1Release(resi) + is.NoError(err) is.Equal(common.StatusPendingUpgrade, res.Info.Status) is.Contains(res.Manifest, "kind: Secret") @@ -587,9 +610,11 @@ func TestUpgradeRelease_DryRun(t *testing.T) { vals = map[string]interface{}{} ctx, done = context.WithCancel(t.Context()) - res, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) + resi, err = upAction.RunWithContext(ctx, rel.Name, buildChart(withSampleSecret()), vals) done() req.NoError(err) + res, err = releaserToV1Release(resi) + is.NoError(err) is.Equal(common.StatusPendingUpgrade, res.Info.Status) is.NotContains(res.Manifest, "kind: Secret") diff --git a/pkg/cmd/install.go b/pkg/cmd/install.go index 28bfa0b51..295f1ae37 100644 --- a/pkg/cmd/install.go +++ b/pkg/cmd/install.go @@ -323,7 +323,12 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options cancel() }() - return client.RunWithContext(ctx, chartRequested, vals) + ri, err := client.RunWithContext(ctx, chartRequested, vals) + rel, rerr := releaserToV1Release(ri) + if rerr != nil { + return nil, rerr + } + return rel, err } // checkIfInstallable validates if a chart can be installed diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index 1988dc5c7..ea8516b1a 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -73,7 +73,11 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { if outfmt == output.Table { client.ShowResourcesTable = true } - rel, err := client.Run(args[0]) + reli, err := client.Run(args[0]) + if err != nil { + return err + } + rel, err := releaserToV1Release(reli) if err != nil { return err } From ab6e7fa58b06f8d084645f0f6c70186cb188b6ee Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 8 Oct 2025 18:48:34 -0400 Subject: [PATCH 078/118] Removing debug statement Signed-off-by: Matt Farina --- pkg/cmd/status.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/cmd/status.go b/pkg/cmd/status.go index ea8516b1a..f68316c6c 100644 --- a/pkg/cmd/status.go +++ b/pkg/cmd/status.go @@ -145,7 +145,6 @@ func (s statusPrinter) WriteTable(out io.Writer) error { return nil } rel := s.getV1Release() - fmt.Printf("%+v", rel) _, _ = fmt.Fprintf(out, "NAME: %s\n", rel.Name) if !rel.Info.LastDeployed.IsZero() { _, _ = fmt.Fprintf(out, "LAST DEPLOYED: %s\n", rel.Info.LastDeployed.Format(time.ANSIC)) From fb20e96a5633f9829a4d714d999888ef04ac572b Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Fri, 10 Oct 2025 12:49:35 -0400 Subject: [PATCH 079/118] Adding a test/example for the release accessor Signed-off-by: Matt Farina --- pkg/release/common_test.go | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 pkg/release/common_test.go diff --git a/pkg/release/common_test.go b/pkg/release/common_test.go new file mode 100644 index 000000000..db93fb923 --- /dev/null +++ b/pkg/release/common_test.go @@ -0,0 +1,65 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package release + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "helm.sh/helm/v4/pkg/release/common" + rspb "helm.sh/helm/v4/pkg/release/v1" +) + +func TestNewDefaultAccessor(t *testing.T) { + // Testing the default implementation rather than NewAccessor which can be + // overridden by developers. + is := assert.New(t) + + // Create release + info := &rspb.Info{Status: common.StatusDeployed, LastDeployed: time.Now().Add(1000)} + labels := make(map[string]string) + labels["foo"] = "bar" + rel := &rspb.Release{ + Name: "happy-cats", + Version: 2, + Info: info, + Labels: labels, + Namespace: "default", + ApplyMethod: "csa", + } + + // NewDefaultAccessor should not be called directly Instead, NewAccessor should be + // called and it will call NewDefaultAccessor. NewAccessor can be changed to a + // non-default accessor by a user so the test calls the default implementation. + // The accessor provides a means to access data on resources that are different types + // but have the same interface. Instead of properties, methods are used to access + // information. Structs with properties are useful in Go when it comes to marshalling + // and unmarshalling data (e.g. coming and going from JSON or YAML). But, structs + // can't be used with interfaces. The accessors enable access to the underlying data + // in a manner that works with Go interfaces. + accessor, err := NewDefaultAccessor(rel) + is.NoError(err) + + // Verify information + is.Equal(rel.Name, accessor.Name()) + is.Equal(rel.Namespace, accessor.Namespace()) + is.Equal(rel.Version, accessor.Version()) + is.Equal(rel.ApplyMethod, accessor.ApplyMethod()) + is.Equal(rel.Labels, accessor.Labels()) +} From 24159ec8b635fb6bfcc05f046b55cad6c3ac9814 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Fri, 10 Oct 2025 13:16:49 -0400 Subject: [PATCH 080/118] Moving new default accessor functions to internal per feedback Signed-off-by: Matt Farina --- pkg/release/common.go | 8 ++++---- pkg/release/common_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/release/common.go b/pkg/release/common.go index 9e6e1050e..d33c96646 100644 --- a/pkg/release/common.go +++ b/pkg/release/common.go @@ -25,11 +25,11 @@ import ( v1release "helm.sh/helm/v4/pkg/release/v1" ) -var NewAccessor func(rel Releaser) (Accessor, error) = NewDefaultAccessor //nolint:revive +var NewAccessor func(rel Releaser) (Accessor, error) = newDefaultAccessor //nolint:revive -var NewHookAccessor func(rel Hook) (HookAccessor, error) = NewDefaultHookAccessor //nolint:revive +var NewHookAccessor func(rel Hook) (HookAccessor, error) = newDefaultHookAccessor //nolint:revive -func NewDefaultAccessor(rel Releaser) (Accessor, error) { +func newDefaultAccessor(rel Releaser) (Accessor, error) { switch v := rel.(type) { case v1release.Release: return &v1Accessor{&v}, nil @@ -40,7 +40,7 @@ func NewDefaultAccessor(rel Releaser) (Accessor, error) { } } -func NewDefaultHookAccessor(hook Hook) (HookAccessor, error) { +func newDefaultHookAccessor(hook Hook) (HookAccessor, error) { switch h := hook.(type) { case v1release.Hook: return &v1HookAccessor{&h}, nil diff --git a/pkg/release/common_test.go b/pkg/release/common_test.go index db93fb923..e9f8d364a 100644 --- a/pkg/release/common_test.go +++ b/pkg/release/common_test.go @@ -44,7 +44,7 @@ func TestNewDefaultAccessor(t *testing.T) { ApplyMethod: "csa", } - // NewDefaultAccessor should not be called directly Instead, NewAccessor should be + // newDefaultAccessor should not be called directly Instead, NewAccessor should be // called and it will call NewDefaultAccessor. NewAccessor can be changed to a // non-default accessor by a user so the test calls the default implementation. // The accessor provides a means to access data on resources that are different types @@ -53,7 +53,7 @@ func TestNewDefaultAccessor(t *testing.T) { // and unmarshalling data (e.g. coming and going from JSON or YAML). But, structs // can't be used with interfaces. The accessors enable access to the underlying data // in a manner that works with Go interfaces. - accessor, err := NewDefaultAccessor(rel) + accessor, err := newDefaultAccessor(rel) is.NoError(err) // Verify information From 62127b79263a3d23c139a5d0e6cbb666a24b184d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:13:44 +0000 Subject: [PATCH 081/118] chore(deps): bump github.com/gofrs/flock from 0.12.1 to 0.13.0 Bumps [github.com/gofrs/flock](https://github.com/gofrs/flock) from 0.12.1 to 0.13.0. - [Release notes](https://github.com/gofrs/flock/releases) - [Commits](https://github.com/gofrs/flock/compare/v0.12.1...v0.13.0) --- updated-dependencies: - dependency-name: github.com/gofrs/flock dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 30664de3b..1cc33410d 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/fluxcd/cli-utils v0.36.0-flux.14 github.com/foxcpp/go-mockdns v1.1.0 github.com/gobwas/glob v0.2.3 - github.com/gofrs/flock v0.12.1 + github.com/gofrs/flock v0.13.0 github.com/gosuri/uitable v0.0.4 github.com/jmoiron/sqlx v1.4.0 github.com/lib/pq v1.10.9 @@ -166,7 +166,7 @@ require ( golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect + golang.org/x/sys v0.37.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.36.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect diff --git a/go.sum b/go.sum index 02a5171d8..d60de2135 100644 --- a/go.sum +++ b/go.sum @@ -129,8 +129,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= -github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -454,8 +454,8 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= From 9f1f9caa7df9f2df5aeff2b2f0794c141fed3cb2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:23:18 +0000 Subject: [PATCH 082/118] chore(deps): bump golang.org/x/crypto from 0.42.0 to 0.43.0 Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.42.0 to 0.43.0. - [Commits](https://github.com/golang/crypto/compare/v0.42.0...v0.43.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-version: 0.43.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 1b509d6a4..a7405de7e 100644 --- a/go.mod +++ b/go.mod @@ -36,8 +36,8 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.9.0 go.yaml.in/yaml/v3 v3.0.4 - golang.org/x/crypto v0.42.0 - golang.org/x/term v0.35.0 + golang.org/x/crypto v0.43.0 + golang.org/x/term v0.36.0 golang.org/x/text v0.30.0 gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.34.1 @@ -163,7 +163,7 @@ require ( go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/mod v0.28.0 // indirect - golang.org/x/net v0.44.0 // indirect + golang.org/x/net v0.45.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.37.0 // indirect diff --git a/go.sum b/go.sum index 2b69d32a6..4e10851d2 100644 --- a/go.sum +++ b/go.sum @@ -396,8 +396,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= -golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= -golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -419,8 +419,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= -golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= -golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= +golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= +golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -463,8 +463,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= +golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From 20eb6e9b049408b3b12dd204195cf990ae458932 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 8 Oct 2025 08:56:20 -0600 Subject: [PATCH 083/118] fix: empty time values Signed-off-by: Terry Howe --- pkg/cmd/history.go | 70 +++++++++ pkg/cmd/history_test.go | 207 ++++++++++++++++++++++++++ pkg/release/v1/hook.go | 67 +++++++++ pkg/release/v1/hook_test.go | 231 +++++++++++++++++++++++++++++ pkg/release/v1/info.go | 83 +++++++++++ pkg/release/v1/info_test.go | 285 ++++++++++++++++++++++++++++++++++++ 6 files changed, 943 insertions(+) create mode 100644 pkg/release/v1/hook_test.go create mode 100644 pkg/release/v1/info_test.go diff --git a/pkg/cmd/history.go b/pkg/cmd/history.go index fc3c26b02..b294a9da7 100644 --- a/pkg/cmd/history.go +++ b/pkg/cmd/history.go @@ -17,6 +17,7 @@ limitations under the License. package cmd import ( + "encoding/json" "fmt" "io" "strconv" @@ -91,6 +92,75 @@ type releaseInfo struct { Description string `json:"description"` } +// releaseInfoJSON is used for custom JSON marshaling/unmarshaling +type releaseInfoJSON struct { + Revision int `json:"revision"` + Updated *time.Time `json:"updated,omitempty"` + Status string `json:"status"` + Chart string `json:"chart"` + AppVersion string `json:"app_version"` + Description string `json:"description"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It handles empty string time fields by treating them as zero values. +func (r *releaseInfo) UnmarshalJSON(data []byte) error { + // First try to unmarshal into a map to handle empty string time fields + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Replace empty string time fields with nil + if val, ok := raw["updated"]; ok { + if str, ok := val.(string); ok && str == "" { + raw["updated"] = nil + } + } + + // Re-marshal with cleaned data + cleaned, err := json.Marshal(raw) + if err != nil { + return err + } + + // Unmarshal into temporary struct with pointer time field + var tmp releaseInfoJSON + if err := json.Unmarshal(cleaned, &tmp); err != nil { + return err + } + + // Copy values to releaseInfo struct + r.Revision = tmp.Revision + if tmp.Updated != nil { + r.Updated = *tmp.Updated + } + r.Status = tmp.Status + r.Chart = tmp.Chart + r.AppVersion = tmp.AppVersion + r.Description = tmp.Description + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +// It omits zero-value time fields from the JSON output. +func (r releaseInfo) MarshalJSON() ([]byte, error) { + tmp := releaseInfoJSON{ + Revision: r.Revision, + Status: r.Status, + Chart: r.Chart, + AppVersion: r.AppVersion, + Description: r.Description, + } + + if !r.Updated.IsZero() { + tmp.Updated = &r.Updated + } + + return json.Marshal(tmp) +} + type releaseHistory []releaseInfo func (r releaseHistory) WriteJSON(out io.Writer) error { diff --git a/pkg/cmd/history_test.go b/pkg/cmd/history_test.go index a324e8bdd..d8adc2d19 100644 --- a/pkg/cmd/history_test.go +++ b/pkg/cmd/history_test.go @@ -17,8 +17,13 @@ limitations under the License. package cmd import ( + "encoding/json" "fmt" "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "helm.sh/helm/v4/pkg/release/common" release "helm.sh/helm/v4/pkg/release/v1" @@ -124,3 +129,205 @@ func TestHistoryFileCompletion(t *testing.T) { checkFileCompletion(t, "history", false) checkFileCompletion(t, "history myrelease", false) } + +func TestReleaseInfoMarshalJSON(t *testing.T) { + updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + info releaseInfo + expected string + }{ + { + name: "all fields populated", + info: releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + }, + expected: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`, + }, + { + name: "without updated time", + info: releaseInfo{ + Revision: 2, + Status: "superseded", + Chart: "mychart-1.0.1", + AppVersion: "1.0.1", + Description: "Upgraded", + }, + expected: `{"revision":2,"status":"superseded","chart":"mychart-1.0.1","app_version":"1.0.1","description":"Upgraded"}`, + }, + { + name: "with zero revision", + info: releaseInfo{ + Revision: 0, + Updated: updated, + Status: "failed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Install failed", + }, + expected: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"failed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Install failed"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.info) + require.NoError(t, err) + assert.JSONEq(t, tt.expected, string(data)) + }) + } +} + +func TestReleaseInfoUnmarshalJSON(t *testing.T) { + updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + + tests := []struct { + name string + input string + expected releaseInfo + wantErr bool + }{ + { + name: "all fields populated", + input: `{"revision":1,"updated":"2025-10-08T12:00:00Z","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Initial install"}`, + expected: releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + }, + }, + { + name: "empty string updated field", + input: `{"revision":2,"updated":"","status":"superseded","chart":"mychart-1.0.1","app_version":"1.0.1","description":"Upgraded"}`, + expected: releaseInfo{ + Revision: 2, + Status: "superseded", + Chart: "mychart-1.0.1", + AppVersion: "1.0.1", + Description: "Upgraded", + }, + }, + { + name: "missing updated field", + input: `{"revision":3,"status":"deployed","chart":"mychart-1.0.2","app_version":"1.0.2","description":"Upgraded"}`, + expected: releaseInfo{ + Revision: 3, + Status: "deployed", + Chart: "mychart-1.0.2", + AppVersion: "1.0.2", + Description: "Upgraded", + }, + }, + { + name: "null updated field", + input: `{"revision":4,"updated":null,"status":"failed","chart":"mychart-1.0.3","app_version":"1.0.3","description":"Failed"}`, + expected: releaseInfo{ + Revision: 4, + Status: "failed", + Chart: "mychart-1.0.3", + AppVersion: "1.0.3", + Description: "Failed", + }, + }, + { + name: "invalid time format", + input: `{"revision":5,"updated":"invalid-time","status":"deployed","chart":"mychart-1.0.4","app_version":"1.0.4","description":"Test"}`, + wantErr: true, + }, + { + name: "zero revision", + input: `{"revision":0,"updated":"2025-10-08T12:00:00Z","status":"pending-install","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Installing"}`, + expected: releaseInfo{ + Revision: 0, + Updated: updated, + Status: "pending-install", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Installing", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var info releaseInfo + err := json.Unmarshal([]byte(tt.input), &info) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.Revision, info.Revision) + assert.Equal(t, tt.expected.Updated.Unix(), info.Updated.Unix()) + assert.Equal(t, tt.expected.Status, info.Status) + assert.Equal(t, tt.expected.Chart, info.Chart) + assert.Equal(t, tt.expected.AppVersion, info.AppVersion) + assert.Equal(t, tt.expected.Description, info.Description) + }) + } +} + +func TestReleaseInfoRoundTrip(t *testing.T) { + updated := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + + original := releaseInfo{ + Revision: 1, + Updated: updated, + Status: "deployed", + Chart: "mychart-1.0.0", + AppVersion: "1.0.0", + Description: "Initial install", + } + + data, err := json.Marshal(&original) + require.NoError(t, err) + + var decoded releaseInfo + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.Revision, decoded.Revision) + assert.Equal(t, original.Updated.Unix(), decoded.Updated.Unix()) + assert.Equal(t, original.Status, decoded.Status) + assert.Equal(t, original.Chart, decoded.Chart) + assert.Equal(t, original.AppVersion, decoded.AppVersion) + assert.Equal(t, original.Description, decoded.Description) +} + +func TestReleaseInfoEmptyStringRoundTrip(t *testing.T) { + // This test specifically verifies that empty string time fields + // are handled correctly during parsing + input := `{"revision":1,"updated":"","status":"deployed","chart":"mychart-1.0.0","app_version":"1.0.0","description":"Test"}` + + var info releaseInfo + err := json.Unmarshal([]byte(input), &info) + require.NoError(t, err) + + // Verify time field is zero value + assert.True(t, info.Updated.IsZero()) + assert.Equal(t, 1, info.Revision) + assert.Equal(t, "deployed", info.Status) + + // Marshal back and verify empty time field is omitted + data, err := json.Marshal(&info) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Zero time value should be omitted + assert.NotContains(t, result, "updated") + assert.Equal(t, float64(1), result["revision"]) + assert.Equal(t, "deployed", result["status"]) + assert.Equal(t, "mychart-1.0.0", result["chart"]) +} diff --git a/pkg/release/v1/hook.go b/pkg/release/v1/hook.go index b7d3c3992..f0a370c15 100644 --- a/pkg/release/v1/hook.go +++ b/pkg/release/v1/hook.go @@ -17,6 +17,7 @@ limitations under the License. package v1 import ( + "encoding/json" "time" ) @@ -120,3 +121,69 @@ const ( // String converts a hook phase to a printable string func (x HookPhase) String() string { return string(x) } + +// hookExecutionJSON is used for custom JSON marshaling/unmarshaling +type hookExecutionJSON struct { + StartedAt *time.Time `json:"started_at,omitempty"` + CompletedAt *time.Time `json:"completed_at,omitempty"` + Phase HookPhase `json:"phase"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It handles empty string time fields by treating them as zero values. +func (h *HookExecution) UnmarshalJSON(data []byte) error { + // First try to unmarshal into a map to handle empty string time fields + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Replace empty string time fields with nil + for _, field := range []string{"started_at", "completed_at"} { + if val, ok := raw[field]; ok { + if str, ok := val.(string); ok && str == "" { + raw[field] = nil + } + } + } + + // Re-marshal with cleaned data + cleaned, err := json.Marshal(raw) + if err != nil { + return err + } + + // Unmarshal into temporary struct with pointer time fields + var tmp hookExecutionJSON + if err := json.Unmarshal(cleaned, &tmp); err != nil { + return err + } + + // Copy values to HookExecution struct + if tmp.StartedAt != nil { + h.StartedAt = *tmp.StartedAt + } + if tmp.CompletedAt != nil { + h.CompletedAt = *tmp.CompletedAt + } + h.Phase = tmp.Phase + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +// It omits zero-value time fields from the JSON output. +func (h HookExecution) MarshalJSON() ([]byte, error) { + tmp := hookExecutionJSON{ + Phase: h.Phase, + } + + if !h.StartedAt.IsZero() { + tmp.StartedAt = &h.StartedAt + } + if !h.CompletedAt.IsZero() { + tmp.CompletedAt = &h.CompletedAt + } + + return json.Marshal(tmp) +} diff --git a/pkg/release/v1/hook_test.go b/pkg/release/v1/hook_test.go new file mode 100644 index 000000000..cea2568bc --- /dev/null +++ b/pkg/release/v1/hook_test.go @@ -0,0 +1,231 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHookExecutionMarshalJSON(t *testing.T) { + started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC) + + tests := []struct { + name string + exec HookExecution + expected string + }{ + { + name: "all fields populated", + exec: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseSucceeded, + }, + expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`, + }, + { + name: "only phase", + exec: HookExecution{ + Phase: HookPhaseRunning, + }, + expected: `{"phase":"Running"}`, + }, + { + name: "with started time only", + exec: HookExecution{ + StartedAt: started, + Phase: HookPhaseRunning, + }, + expected: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`, + }, + { + name: "failed phase", + exec: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseFailed, + }, + expected: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`, + }, + { + name: "unknown phase", + exec: HookExecution{ + Phase: HookPhaseUnknown, + }, + expected: `{"phase":"Unknown"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.exec) + require.NoError(t, err) + assert.JSONEq(t, tt.expected, string(data)) + }) + } +} + +func TestHookExecutionUnmarshalJSON(t *testing.T) { + started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC) + + tests := []struct { + name string + input string + expected HookExecution + wantErr bool + }{ + { + name: "all fields populated", + input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Succeeded"}`, + expected: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseSucceeded, + }, + }, + { + name: "only phase", + input: `{"phase":"Running"}`, + expected: HookExecution{ + Phase: HookPhaseRunning, + }, + }, + { + name: "empty string time fields", + input: `{"started_at":"","completed_at":"","phase":"Succeeded"}`, + expected: HookExecution{ + Phase: HookPhaseSucceeded, + }, + }, + { + name: "missing time fields", + input: `{"phase":"Failed"}`, + expected: HookExecution{ + Phase: HookPhaseFailed, + }, + }, + { + name: "null time fields", + input: `{"started_at":null,"completed_at":null,"phase":"Unknown"}`, + expected: HookExecution{ + Phase: HookPhaseUnknown, + }, + }, + { + name: "mixed empty and valid time fields", + input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"","phase":"Running"}`, + expected: HookExecution{ + StartedAt: started, + Phase: HookPhaseRunning, + }, + }, + { + name: "with started time only", + input: `{"started_at":"2025-10-08T12:00:00Z","phase":"Running"}`, + expected: HookExecution{ + StartedAt: started, + Phase: HookPhaseRunning, + }, + }, + { + name: "failed phase with times", + input: `{"started_at":"2025-10-08T12:00:00Z","completed_at":"2025-10-08T12:05:00Z","phase":"Failed"}`, + expected: HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseFailed, + }, + }, + { + name: "invalid time format", + input: `{"started_at":"invalid-time","phase":"Running"}`, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var exec HookExecution + err := json.Unmarshal([]byte(tt.input), &exec) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.StartedAt.Unix(), exec.StartedAt.Unix()) + assert.Equal(t, tt.expected.CompletedAt.Unix(), exec.CompletedAt.Unix()) + assert.Equal(t, tt.expected.Phase, exec.Phase) + }) + } +} + +func TestHookExecutionRoundTrip(t *testing.T) { + started := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + completed := time.Date(2025, 10, 8, 12, 5, 0, 0, time.UTC) + + original := HookExecution{ + StartedAt: started, + CompletedAt: completed, + Phase: HookPhaseSucceeded, + } + + data, err := json.Marshal(&original) + require.NoError(t, err) + + var decoded HookExecution + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.StartedAt.Unix(), decoded.StartedAt.Unix()) + assert.Equal(t, original.CompletedAt.Unix(), decoded.CompletedAt.Unix()) + assert.Equal(t, original.Phase, decoded.Phase) +} + +func TestHookExecutionEmptyStringRoundTrip(t *testing.T) { + // This test specifically verifies that empty string time fields + // are handled correctly during parsing + input := `{"started_at":"","completed_at":"","phase":"Succeeded"}` + + var exec HookExecution + err := json.Unmarshal([]byte(input), &exec) + require.NoError(t, err) + + // Verify time fields are zero values + assert.True(t, exec.StartedAt.IsZero()) + assert.True(t, exec.CompletedAt.IsZero()) + assert.Equal(t, HookPhaseSucceeded, exec.Phase) + + // Marshal back and verify empty time fields are omitted + data, err := json.Marshal(&exec) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Zero time values should be omitted + assert.NotContains(t, result, "started_at") + assert.NotContains(t, result, "completed_at") + assert.Equal(t, "Succeeded", result["phase"]) +} diff --git a/pkg/release/v1/info.go b/pkg/release/v1/info.go index f502b7bc9..f895fdf6c 100644 --- a/pkg/release/v1/info.go +++ b/pkg/release/v1/info.go @@ -16,6 +16,7 @@ limitations under the License. package v1 import ( + "encoding/json" "time" "helm.sh/helm/v4/pkg/release/common" @@ -40,3 +41,85 @@ type Info struct { // Contains the deployed resources information Resources map[string][]runtime.Object `json:"resources,omitempty"` } + +// infoJSON is used for custom JSON marshaling/unmarshaling +type infoJSON struct { + FirstDeployed *time.Time `json:"first_deployed,omitempty"` + LastDeployed *time.Time `json:"last_deployed,omitempty"` + Deleted *time.Time `json:"deleted,omitempty"` + Description string `json:"description,omitempty"` + Status common.Status `json:"status,omitempty"` + Notes string `json:"notes,omitempty"` + Resources map[string][]runtime.Object `json:"resources,omitempty"` +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +// It handles empty string time fields by treating them as zero values. +func (i *Info) UnmarshalJSON(data []byte) error { + // First try to unmarshal into a map to handle empty string time fields + var raw map[string]interface{} + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + + // Replace empty string time fields with nil + for _, field := range []string{"first_deployed", "last_deployed", "deleted"} { + if val, ok := raw[field]; ok { + if str, ok := val.(string); ok && str == "" { + raw[field] = nil + } + } + } + + // Re-marshal with cleaned data + cleaned, err := json.Marshal(raw) + if err != nil { + return err + } + + // Unmarshal into temporary struct with pointer time fields + var tmp infoJSON + if err := json.Unmarshal(cleaned, &tmp); err != nil { + return err + } + + // Copy values to Info struct + if tmp.FirstDeployed != nil { + i.FirstDeployed = *tmp.FirstDeployed + } + if tmp.LastDeployed != nil { + i.LastDeployed = *tmp.LastDeployed + } + if tmp.Deleted != nil { + i.Deleted = *tmp.Deleted + } + i.Description = tmp.Description + i.Status = tmp.Status + i.Notes = tmp.Notes + i.Resources = tmp.Resources + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +// It omits zero-value time fields from the JSON output. +func (i Info) MarshalJSON() ([]byte, error) { + tmp := infoJSON{ + Description: i.Description, + Status: i.Status, + Notes: i.Notes, + Resources: i.Resources, + } + + if !i.FirstDeployed.IsZero() { + tmp.FirstDeployed = &i.FirstDeployed + } + if !i.LastDeployed.IsZero() { + tmp.LastDeployed = &i.LastDeployed + } + if !i.Deleted.IsZero() { + tmp.Deleted = &i.Deleted + } + + return json.Marshal(tmp) +} diff --git a/pkg/release/v1/info_test.go b/pkg/release/v1/info_test.go new file mode 100644 index 000000000..0fff78f76 --- /dev/null +++ b/pkg/release/v1/info_test.go @@ -0,0 +1,285 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1 + +import ( + "encoding/json" + "testing" + "time" + + "helm.sh/helm/v4/pkg/release/common" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInfoMarshalJSON(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC) + + tests := []struct { + name string + info Info + expected string + }{ + { + name: "all fields populated", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Description: "Test release", + Status: common.StatusDeployed, + Notes: "Test notes", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`, + }, + { + name: "only required fields", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`, + }, + { + name: "zero time values omitted", + info: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + expected: `{"description":"Test release","status":"deployed"}`, + }, + { + name: "with pending status", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusPendingInstall, + Description: "Installing release", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","description":"Installing release","status":"pending-install"}`, + }, + { + name: "uninstalled with deleted time", + info: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Status: common.StatusUninstalled, + Description: "Uninstalled release", + }, + expected: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Uninstalled release","status":"uninstalled"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(&tt.info) + require.NoError(t, err) + assert.JSONEq(t, tt.expected, string(data)) + }) + } +} + +func TestInfoUnmarshalJSON(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + deleted := time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC) + + tests := []struct { + name string + input string + expected Info + wantErr bool + }{ + { + name: "all fields populated", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","description":"Test release","status":"deployed","notes":"Test notes"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Description: "Test release", + Status: common.StatusDeployed, + Notes: "Test notes", + }, + }, + { + name: "only required fields", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"deployed"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusDeployed, + }, + }, + { + name: "empty string time fields", + input: `{"first_deployed":"","last_deployed":"","deleted":"","description":"Test release","status":"deployed"}`, + expected: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + }, + { + name: "missing time fields", + input: `{"description":"Test release","status":"deployed"}`, + expected: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + }, + { + name: "null time fields", + input: `{"first_deployed":null,"last_deployed":null,"deleted":null,"description":"Test release","status":"deployed"}`, + expected: Info{ + Description: "Test release", + Status: common.StatusDeployed, + }, + }, + { + name: "mixed empty and valid time fields", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"","deleted":"","status":"deployed"}`, + expected: Info{ + FirstDeployed: now, + Status: common.StatusDeployed, + }, + }, + { + name: "pending install status", + input: `{"first_deployed":"2025-10-08T12:00:00Z","status":"pending-install","description":"Installing"}`, + expected: Info{ + FirstDeployed: now, + Status: common.StatusPendingInstall, + Description: "Installing", + }, + }, + { + name: "uninstalled with deleted time", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","deleted":"2025-10-08T14:00:00Z","status":"uninstalled"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Deleted: deleted, + Status: common.StatusUninstalled, + }, + }, + { + name: "failed status", + input: `{"first_deployed":"2025-10-08T12:00:00Z","last_deployed":"2025-10-08T13:00:00Z","status":"failed","description":"Deployment failed"}`, + expected: Info{ + FirstDeployed: now, + LastDeployed: later, + Status: common.StatusFailed, + Description: "Deployment failed", + }, + }, + { + name: "invalid time format", + input: `{"first_deployed":"invalid-time","status":"deployed"}`, + wantErr: true, + }, + { + name: "empty object", + input: `{}`, + expected: Info{ + Status: "", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var info Info + err := json.Unmarshal([]byte(tt.input), &info) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected.FirstDeployed.Unix(), info.FirstDeployed.Unix()) + assert.Equal(t, tt.expected.LastDeployed.Unix(), info.LastDeployed.Unix()) + assert.Equal(t, tt.expected.Deleted.Unix(), info.Deleted.Unix()) + assert.Equal(t, tt.expected.Description, info.Description) + assert.Equal(t, tt.expected.Status, info.Status) + assert.Equal(t, tt.expected.Notes, info.Notes) + assert.Equal(t, tt.expected.Resources, info.Resources) + }) + } +} + +func TestInfoRoundTrip(t *testing.T) { + now := time.Date(2025, 10, 8, 12, 0, 0, 0, time.UTC) + later := time.Date(2025, 10, 8, 13, 0, 0, 0, time.UTC) + + original := Info{ + FirstDeployed: now, + LastDeployed: later, + Description: "Test release", + Status: common.StatusDeployed, + Notes: "Release notes", + } + + data, err := json.Marshal(&original) + require.NoError(t, err) + + var decoded Info + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.FirstDeployed.Unix(), decoded.FirstDeployed.Unix()) + assert.Equal(t, original.LastDeployed.Unix(), decoded.LastDeployed.Unix()) + assert.Equal(t, original.Deleted.Unix(), decoded.Deleted.Unix()) + assert.Equal(t, original.Description, decoded.Description) + assert.Equal(t, original.Status, decoded.Status) + assert.Equal(t, original.Notes, decoded.Notes) +} + +func TestInfoEmptyStringRoundTrip(t *testing.T) { + // This test specifically verifies that empty string time fields + // are handled correctly during parsing + input := `{"first_deployed":"","last_deployed":"","deleted":"","status":"deployed","description":"test"}` + + var info Info + err := json.Unmarshal([]byte(input), &info) + require.NoError(t, err) + + // Verify time fields are zero values + assert.True(t, info.FirstDeployed.IsZero()) + assert.True(t, info.LastDeployed.IsZero()) + assert.True(t, info.Deleted.IsZero()) + assert.Equal(t, common.StatusDeployed, info.Status) + assert.Equal(t, "test", info.Description) + + // Marshal back and verify empty time fields are omitted + data, err := json.Marshal(&info) + require.NoError(t, err) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + // Zero time values should be omitted due to omitzero tag + assert.NotContains(t, result, "first_deployed") + assert.NotContains(t, result, "last_deployed") + assert.NotContains(t, result, "deleted") + assert.Equal(t, "deployed", result["status"]) + assert.Equal(t, "test", result["description"]) +} From 06188f9dfe6f26bc61db23d4b4cf23f673dd20f5 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Sat, 11 Oct 2025 13:28:36 +0200 Subject: [PATCH 084/118] Add test for isYamlFileExtension functions Signed-off-by: Benoit Tigeot --- internal/chart/v3/lint/rules/template_test.go | 20 +++++++++++++++++++ pkg/chart/v2/lint/rules/template_test.go | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/internal/chart/v3/lint/rules/template_test.go b/internal/chart/v3/lint/rules/template_test.go index 40bcfa26b..d7665211a 100644 --- a/internal/chart/v3/lint/rules/template_test.go +++ b/internal/chart/v3/lint/rules/template_test.go @@ -439,3 +439,23 @@ items: t.Fatalf("List objects keep annotations should pass. got: %s", err) } } + +func TestIsYamlFileExtension(t *testing.T) { + tests := []struct { + filename string + expected bool + }{ + {"test.yaml", true}, + {"test.yml", true}, + {"test.txt", false}, + {"test", false}, + } + + for _, test := range tests { + result := isYamlFileExtension(test.filename) + if result != test.expected { + t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected) + } + } + +} diff --git a/pkg/chart/v2/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go index 3e8e0b371..8354f9e4c 100644 --- a/pkg/chart/v2/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -439,3 +439,23 @@ items: t.Fatalf("List objects keep annotations should pass. got: %s", err) } } + +func TestIsYamlFileExtension(t *testing.T) { + tests := []struct { + filename string + expected bool + }{ + {"test.yaml", true}, + {"test.yml", true}, + {"test.txt", false}, + {"test", false}, + } + + for _, test := range tests { + result := isYamlFileExtension(test.filename) + if result != test.expected { + t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected) + } + } + +} From 9cd40c612acd33a32830aa36a618616cefa2f068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dirk=20M=C3=BCller?= Date: Sun, 12 Oct 2025 19:24:42 +0200 Subject: [PATCH 085/118] Avoid accessing .Items on nil object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When listing fails for whatever reason, the return value is nil, err. so handle err explicitly. Signed-off-by: Dirk Müller --- pkg/kube/ready.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/kube/ready.go b/pkg/kube/ready.go index 7a06c72f9..42e327bdd 100644 --- a/pkg/kube/ready.go +++ b/pkg/kube/ready.go @@ -455,5 +455,8 @@ func getPods(ctx context.Context, client kubernetes.Interface, namespace, select list, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ LabelSelector: selector, }) - return list.Items, err + if err != nil { + return nil, fmt.Errorf("failed to list pods: %w", err) + } + return list.Items, nil } From 8a73d97984f934d6c92e1eecce852c125242d6e1 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 15 Sep 2025 14:27:17 -0600 Subject: [PATCH 086/118] fix: make helm list show all by default Signed-off-by: Terry Howe --- pkg/action/list.go | 6 ++-- pkg/action/list_test.go | 10 ++++-- pkg/cmd/list.go | 22 +++++++------ pkg/cmd/list_test.go | 32 +++++++++---------- .../output/list-all-date-reversed.txt | 10 ++++++ pkg/cmd/testdata/output/list-all-date.txt | 9 ++++++ pkg/cmd/testdata/output/list-all-max.txt | 2 ++ .../testdata/output/list-all-no-headers.txt | 8 +++++ pkg/cmd/testdata/output/list-all-offset.txt | 8 +++++ pkg/cmd/testdata/output/list-all-reverse.txt | 9 ++++++ .../testdata/output/list-all-short-json.txt | 1 + .../testdata/output/list-all-short-yaml.txt | 8 +++++ pkg/cmd/testdata/output/list-all-short.txt | 8 +++++ 13 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 pkg/cmd/testdata/output/list-all-date-reversed.txt create mode 100644 pkg/cmd/testdata/output/list-all-date.txt create mode 100644 pkg/cmd/testdata/output/list-all-max.txt create mode 100644 pkg/cmd/testdata/output/list-all-no-headers.txt create mode 100644 pkg/cmd/testdata/output/list-all-offset.txt create mode 100644 pkg/cmd/testdata/output/list-all-reverse.txt create mode 100644 pkg/cmd/testdata/output/list-all-short-json.txt create mode 100644 pkg/cmd/testdata/output/list-all-short-yaml.txt create mode 100644 pkg/cmd/testdata/output/list-all-short.txt diff --git a/pkg/action/list.go b/pkg/action/list.go index ea743db60..b62a18e7f 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -140,7 +140,7 @@ type List struct { // NewList constructs a new *List func NewList(cfg *Configuration) *List { return &List{ - StateMask: ListDeployed | ListFailed, + StateMask: ListAll, cfg: cfg, } } @@ -325,9 +325,9 @@ func (l *List) SetStateMask() { state |= ListSuperseded } - // Apply a default + // Apply a default - now defaults to ListAll instead of just deployed and failed if state == 0 { - state = ListDeployed | ListFailed + state = ListAll } l.StateMask = state diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index bf34b0ba1..a1fc150e3 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -196,17 +196,21 @@ func TestList_StateMask(t *testing.T) { err = lister.cfg.Releases.Update(one) is.NoError(err) + // With the new default (ListAll), we should see all 3 releases by default res, err := lister.Run() is.NoError(err) - is.Len(res, 2) + is.Len(res, 3) ac0, err := ri.NewAccessor(res[0]) is.NoError(err) ac1, err := ri.NewAccessor(res[1]) is.NoError(err) + ac2, err := ri.NewAccessor(res[2]) + is.NoError(err) - is.Equal("three", ac0.Name()) - is.Equal("two", ac1.Name()) + is.Equal("one", ac0.Name()) + is.Equal("three", ac1.Name()) + is.Equal("two", ac2.Name()) lister.StateMask = ListUninstalled res, err = lister.Run() diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 99e0bc9b8..df41e008b 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -37,9 +37,12 @@ import ( var listHelp = ` This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). -By default, it lists only releases that are deployed or failed. Flags like -'--uninstalled' and '--all' will alter this behavior. Such flags can be combined: -'--uninstalled --failed'. +By default, it lists all releases in any status including deployed, failed, +pending-upgrade, pending-install, pending-rollback, uninstalled, uninstalling, +superseded, and unknown. Individual status filters like '--deployed', '--failed', +'--pending', '--uninstalled', '--superseded', and '--uninstalling' can be used +to show only releases in specific states. Such flags can be combined: +'--deployed --failed'. By default, items are sorted alphabetically. Use the '-d' flag to sort by release date. @@ -122,13 +125,12 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`) f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") - f.BoolVarP(&client.All, "all", "a", false, "show all releases without any filter applied") - f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)") - f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") - f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") - f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") - f.BoolVar(&client.Failed, "failed", false, "show failed releases") - f.BoolVar(&client.Pending, "pending", false, "show pending releases") + f.BoolVar(&client.Uninstalled, "uninstalled", false, "show only uninstalled releases (if 'helm uninstall --keep-history' was used)") + f.BoolVar(&client.Superseded, "superseded", false, "show only superseded releases") + f.BoolVar(&client.Uninstalling, "uninstalling", false, "show only releases that are currently being uninstalled") + f.BoolVar(&client.Deployed, "deployed", false, "show only deployed releases") + f.BoolVar(&client.Failed, "failed", false, "show only failed releases") + f.BoolVar(&client.Pending, "pending", false, "show only pending releases") f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces") f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value") diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index 42e9e2083..097e62d11 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -147,22 +147,17 @@ func TestListCmd(t *testing.T) { tests := []cmdTestCase{{ name: "list releases", cmd: "list", - golden: "output/list.txt", + golden: "output/list-all.txt", rels: releaseFixture, }, { name: "list without headers", cmd: "list --no-headers", - golden: "output/list-no-headers.txt", - rels: releaseFixture, - }, { - name: "list all releases", - cmd: "list --all", - golden: "output/list-all.txt", + golden: "output/list-all-no-headers.txt", rels: releaseFixture, }, { name: "list releases sorted by release date", cmd: "list --date", - golden: "output/list-date.txt", + golden: "output/list-all-date.txt", rels: releaseFixture, }, { name: "list failed releases", @@ -172,17 +167,17 @@ func TestListCmd(t *testing.T) { }, { name: "list filtered releases", cmd: "list --filter='.*'", - golden: "output/list-filter.txt", + golden: "output/list-all.txt", rels: releaseFixture, }, { name: "list releases, limited to one release", cmd: "list --max 1", - golden: "output/list-max.txt", + golden: "output/list-all-max.txt", rels: releaseFixture, }, { name: "list releases, offset by one", cmd: "list --offset 1", - golden: "output/list-offset.txt", + golden: "output/list-all-offset.txt", rels: releaseFixture, }, { name: "list pending releases", @@ -192,27 +187,32 @@ func TestListCmd(t *testing.T) { }, { name: "list releases in reverse order", cmd: "list --reverse", - golden: "output/list-reverse.txt", + golden: "output/list-all-reverse.txt", rels: releaseFixture, }, { name: "list releases sorted by reversed release date", cmd: "list --date --reverse", - golden: "output/list-date-reversed.txt", + golden: "output/list-all-date-reversed.txt", rels: releaseFixture, }, { name: "list releases in short output format", cmd: "list --short", - golden: "output/list-short.txt", + golden: "output/list-all-short.txt", rels: releaseFixture, }, { name: "list releases in short output format", cmd: "list --short --output yaml", - golden: "output/list-short-yaml.txt", + golden: "output/list-all-short-yaml.txt", rels: releaseFixture, }, { name: "list releases in short output format", cmd: "list --short --output json", - golden: "output/list-short-json.txt", + golden: "output/list-all-short-json.txt", + rels: releaseFixture, + }, { + name: "list deployed and failed releases only", + cmd: "list --deployed --failed", + golden: "output/list.txt", rels: releaseFixture, }, { name: "list superseded releases", diff --git a/pkg/cmd/testdata/output/list-all-date-reversed.txt b/pkg/cmd/testdata/output/list-all-date-reversed.txt new file mode 100644 index 000000000..ac659b702 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-date-reversed.txt @@ -0,0 +1,10 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 + diff --git a/pkg/cmd/testdata/output/list-all-date.txt b/pkg/cmd/testdata/output/list-all-date.txt new file mode 100644 index 000000000..b0218c6c4 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-date.txt @@ -0,0 +1,9 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 diff --git a/pkg/cmd/testdata/output/list-all-max.txt b/pkg/cmd/testdata/output/list-all-max.txt new file mode 100644 index 000000000..922896391 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-max.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 diff --git a/pkg/cmd/testdata/output/list-all-no-headers.txt b/pkg/cmd/testdata/output/list-all-no-headers.txt new file mode 100644 index 000000000..33581d8c5 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-no-headers.txt @@ -0,0 +1,8 @@ +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 diff --git a/pkg/cmd/testdata/output/list-all-offset.txt b/pkg/cmd/testdata/output/list-all-offset.txt new file mode 100644 index 000000000..e17fd7b00 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-offset.txt @@ -0,0 +1,8 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 diff --git a/pkg/cmd/testdata/output/list-all-reverse.txt b/pkg/cmd/testdata/output/list-all-reverse.txt new file mode 100644 index 000000000..31bb3de96 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-reverse.txt @@ -0,0 +1,9 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 +iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 +hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 diff --git a/pkg/cmd/testdata/output/list-all-short-json.txt b/pkg/cmd/testdata/output/list-all-short-json.txt new file mode 100644 index 000000000..6dac52c43 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-short-json.txt @@ -0,0 +1 @@ +["drax","gamora","groot","hummingbird","iguana","rocket","starlord","thanos"] diff --git a/pkg/cmd/testdata/output/list-all-short-yaml.txt b/pkg/cmd/testdata/output/list-all-short-yaml.txt new file mode 100644 index 000000000..2ae0e88ad --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-short-yaml.txt @@ -0,0 +1,8 @@ +- drax +- gamora +- groot +- hummingbird +- iguana +- rocket +- starlord +- thanos diff --git a/pkg/cmd/testdata/output/list-all-short.txt b/pkg/cmd/testdata/output/list-all-short.txt new file mode 100644 index 000000000..52871d8b4 --- /dev/null +++ b/pkg/cmd/testdata/output/list-all-short.txt @@ -0,0 +1,8 @@ +drax +gamora +groot +hummingbird +iguana +rocket +starlord +thanos From b4f932a7154464af6b124fc02462158c14a44e04 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 15 Sep 2025 20:54:22 +0000 Subject: [PATCH 087/118] Fix test by making date sorting stable and updating golden files Signed-off-by: Terry Howe --- pkg/cmd/testdata/output/list-all-date-reversed.txt | 7 +++---- pkg/release/v1/util/sorter.go | 6 +++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pkg/cmd/testdata/output/list-all-date-reversed.txt b/pkg/cmd/testdata/output/list-all-date-reversed.txt index ac659b702..d185334a2 100644 --- a/pkg/cmd/testdata/output/list-all-date-reversed.txt +++ b/pkg/cmd/testdata/output/list-all-date-reversed.txt @@ -2,9 +2,8 @@ NAME NAMESPACE REVISION UPDATED STATUS CHA iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 -gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 -groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 - diff --git a/pkg/release/v1/util/sorter.go b/pkg/release/v1/util/sorter.go index 3712a58ef..47506fbf2 100644 --- a/pkg/release/v1/util/sorter.go +++ b/pkg/release/v1/util/sorter.go @@ -44,7 +44,11 @@ func SortByDate(list []*rspb.Release) { sort.Slice(list, func(i, j int) bool { ti := list[i].Info.LastDeployed.Unix() tj := list[j].Info.LastDeployed.Unix() - return ti < tj + if ti != tj { + return ti < tj + } + // Use name as tie-breaker for stable sorting + return list[i].Name < list[j].Name }) } From c3aa5c51e6b1cac330e6ddc595b07cde0f439a69 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 17 Sep 2025 06:42:19 -0600 Subject: [PATCH 088/118] chore: clean up for comments Signed-off-by: Terry Howe --- pkg/action/list.go | 2 +- pkg/action/list_test.go | 1 - pkg/cmd/list.go | 18 +++++++++--------- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pkg/action/list.go b/pkg/action/list.go index b62a18e7f..06727bd9a 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -325,7 +325,7 @@ func (l *List) SetStateMask() { state |= ListSuperseded } - // Apply a default - now defaults to ListAll instead of just deployed and failed + // Apply a default if state == 0 { state = ListAll } diff --git a/pkg/action/list_test.go b/pkg/action/list_test.go index a1fc150e3..643bcea42 100644 --- a/pkg/action/list_test.go +++ b/pkg/action/list_test.go @@ -196,7 +196,6 @@ func TestList_StateMask(t *testing.T) { err = lister.cfg.Releases.Update(one) is.NoError(err) - // With the new default (ListAll), we should see all 3 releases by default res, err := lister.Run() is.NoError(err) is.Len(res, 3) diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index df41e008b..424f3f7d3 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -37,10 +37,10 @@ import ( var listHelp = ` This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). -By default, it lists all releases in any status including deployed, failed, -pending-upgrade, pending-install, pending-rollback, uninstalled, uninstalling, +By default, it lists all releases in any status including deployed, failed, +pending-upgrade, pending-install, pending-rollback, uninstalled, uninstalling, superseded, and unknown. Individual status filters like '--deployed', '--failed', -'--pending', '--uninstalled', '--superseded', and '--uninstalling' can be used +'--pending', '--uninstalled', '--superseded', and '--uninstalling' can be used to show only releases in specific states. Such flags can be combined: '--deployed --failed'. @@ -125,12 +125,12 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringVar(&client.TimeFormat, "time-format", "", `format time using golang time formatter. Example: --time-format "2006-01-02 15:04:05Z0700"`) f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") - f.BoolVar(&client.Uninstalled, "uninstalled", false, "show only uninstalled releases (if 'helm uninstall --keep-history' was used)") - f.BoolVar(&client.Superseded, "superseded", false, "show only superseded releases") - f.BoolVar(&client.Uninstalling, "uninstalling", false, "show only releases that are currently being uninstalled") - f.BoolVar(&client.Deployed, "deployed", false, "show only deployed releases") - f.BoolVar(&client.Failed, "failed", false, "show only failed releases") - f.BoolVar(&client.Pending, "pending", false, "show only pending releases") + f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)") + f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") + f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") + f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases") + f.BoolVar(&client.Failed, "failed", false, "show failed releases") + f.BoolVar(&client.Pending, "pending", false, "show pending releases") f.BoolVarP(&client.AllNamespaces, "all-namespaces", "A", false, "list releases across all namespaces") f.IntVarP(&client.Limit, "max", "m", 256, "maximum number of releases to fetch") f.IntVar(&client.Offset, "offset", 0, "next release index in the list, used to offset from start value") From f6e60138e809be6d429d1ee5bc0ee3a290e12fac Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Tue, 23 Sep 2025 10:04:17 -0600 Subject: [PATCH 089/118] Update pkg/cmd/list.go Co-authored-by: George Jenkins Signed-off-by: Terry Howe --- pkg/cmd/list.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/cmd/list.go b/pkg/cmd/list.go index 424f3f7d3..3c15a0954 100644 --- a/pkg/cmd/list.go +++ b/pkg/cmd/list.go @@ -37,9 +37,7 @@ import ( var listHelp = ` This command lists all of the releases for a specified namespace (uses current namespace context if namespace not specified). -By default, it lists all releases in any status including deployed, failed, -pending-upgrade, pending-install, pending-rollback, uninstalled, uninstalling, -superseded, and unknown. Individual status filters like '--deployed', '--failed', +By default, it lists all releases in any status. Individual status filters like '--deployed', '--failed', '--pending', '--uninstalled', '--superseded', and '--uninstalling' can be used to show only releases in specific states. Such flags can be combined: '--deployed --failed'. From abdf6603c08b53ba7f67c0442351ca1989baa9e7 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 13 Oct 2025 11:15:26 -0600 Subject: [PATCH 090/118] fix test for rebase Signed-off-by: Terry Howe --- pkg/cmd/testdata/output/list-all-date.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/cmd/testdata/output/list-all-date.txt b/pkg/cmd/testdata/output/list-all-date.txt index b0218c6c4..5e5f9efee 100644 --- a/pkg/cmd/testdata/output/list-all-date.txt +++ b/pkg/cmd/testdata/output/list-all-date.txt @@ -1,9 +1,9 @@ NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION drax default 1 2016-01-16 00:00:01 +0000 UTC uninstalling chickadee-1.0.0 0.0.1 -thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 -starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 -groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 gamora default 1 2016-01-16 00:00:01 +0000 UTC superseded chickadee-1.0.0 0.0.1 +groot default 1 2016-01-16 00:00:01 +0000 UTC uninstalled chickadee-1.0.0 0.0.1 +starlord default 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1 +thanos default 1 2016-01-16 00:00:01 +0000 UTC pending-install chickadee-1.0.0 0.0.1 rocket default 1 2016-01-16 00:00:02 +0000 UTC failed chickadee-1.0.0 0.0.1 hummingbird default 1 2016-01-16 00:00:03 +0000 UTC deployed chickadee-1.0.0 0.0.1 iguana default 2 2016-01-16 00:00:04 +0000 UTC deployed chickadee-1.0.0 0.0.1 From b52ce5c0cb94dd07089334469a75901c0bfc11d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Oct 2025 21:15:15 +0000 Subject: [PATCH 091/118] chore(deps): bump sigs.k8s.io/controller-runtime from 0.22.2 to 0.22.3 Bumps [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) from 0.22.2 to 0.22.3. - [Release notes](https://github.com/kubernetes-sigs/controller-runtime/releases) - [Changelog](https://github.com/kubernetes-sigs/controller-runtime/blob/main/RELEASE.md) - [Commits](https://github.com/kubernetes-sigs/controller-runtime/compare/v0.22.2...v0.22.3) --- updated-dependencies: - dependency-name: sigs.k8s.io/controller-runtime dependency-version: 0.22.3 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 a7405de7e..858c42fe4 100644 --- a/go.mod +++ b/go.mod @@ -49,7 +49,7 @@ require ( k8s.io/klog/v2 v2.130.1 k8s.io/kubectl v0.34.1 oras.land/oras-go/v2 v2.6.0 - sigs.k8s.io/controller-runtime v0.22.2 + sigs.k8s.io/controller-runtime v0.22.3 sigs.k8s.io/kustomize/kyaml v0.20.1 sigs.k8s.io/yaml v1.6.0 ) diff --git a/go.sum b/go.sum index 4e10851d2..bbb849a4f 100644 --- a/go.sum +++ b/go.sum @@ -536,8 +536,8 @@ k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8 k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= -sigs.k8s.io/controller-runtime v0.22.2 h1:cK2l8BGWsSWkXz09tcS4rJh95iOLney5eawcK5A33r4= -sigs.k8s.io/controller-runtime v0.22.2/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= +sigs.k8s.io/controller-runtime v0.22.3 h1:I7mfqz/a/WdmDCEnXmSPm8/b/yRTy6JsKKENTijTq8Y= +sigs.k8s.io/controller-runtime v0.22.3/go.mod h1:+QX1XUpTXN4mLoblf4tqr5CQcyHPAki2HLXqQMY6vh8= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= From 379b0899b3f068949700684840da01496ff344f1 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Wed, 17 Sep 2025 06:09:02 -0600 Subject: [PATCH 092/118] fix: increase helm list test coverage Signed-off-by: Terry Howe --- pkg/cmd/list_test.go | 370 +++++++++++++++++++ pkg/cmd/testdata/output/list-json.txt | 1 + pkg/cmd/testdata/output/list-time-format.txt | 2 + pkg/cmd/testdata/output/list-yaml.txt | 7 + 4 files changed, 380 insertions(+) create mode 100644 pkg/cmd/testdata/output/list-json.txt create mode 100644 pkg/cmd/testdata/output/list-time-format.txt create mode 100644 pkg/cmd/testdata/output/list-yaml.txt diff --git a/pkg/cmd/list_test.go b/pkg/cmd/list_test.go index 097e62d11..35153465a 100644 --- a/pkg/cmd/list_test.go +++ b/pkg/cmd/list_test.go @@ -245,3 +245,373 @@ func TestListOutputCompletion(t *testing.T) { func TestListFileCompletion(t *testing.T) { checkFileCompletion(t, "list", false) } + +func TestListOutputFormats(t *testing.T) { + defaultNamespace := "default" + timestamp := time.Unix(1452902400, 0).UTC() + chartInfo := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "test-chart", + Version: "1.0.0", + AppVersion: "0.0.1", + }, + } + + releaseFixture := []*release.Release{ + { + Name: "test-release", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp, + Status: common.StatusDeployed, + }, + Chart: chartInfo, + }, + } + + tests := []cmdTestCase{{ + name: "list releases in json format", + cmd: "list --output json", + golden: "output/list-json.txt", + rels: releaseFixture, + }, { + name: "list releases in yaml format", + cmd: "list --output yaml", + golden: "output/list-yaml.txt", + rels: releaseFixture, + }} + runTestCmd(t, tests) +} + +func TestReleaseListWriter(t *testing.T) { + timestamp := time.Unix(1452902400, 0).UTC() + chartInfo := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "test-chart", + Version: "1.0.0", + AppVersion: "0.0.1", + }, + } + + releases := []*release.Release{ + { + Name: "test-release", + Version: 1, + Namespace: "default", + Info: &release.Info{ + LastDeployed: timestamp, + Status: common.StatusDeployed, + }, + Chart: chartInfo, + }, + } + + tests := []struct { + name string + releases []*release.Release + timeFormat string + noHeaders bool + noColor bool + }{ + { + name: "empty releases list", + releases: []*release.Release{}, + timeFormat: "", + noHeaders: false, + noColor: false, + }, + { + name: "custom time format", + releases: releases, + timeFormat: "2006-01-02", + noHeaders: false, + noColor: false, + }, + { + name: "no headers", + releases: releases, + timeFormat: "", + noHeaders: true, + noColor: false, + }, + { + name: "no color", + releases: releases, + timeFormat: "", + noHeaders: false, + noColor: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + writer := newReleaseListWriter(tt.releases, tt.timeFormat, tt.noHeaders, tt.noColor) + + if writer == nil { + t.Error("Expected writer to be non-nil") + } else { + if len(writer.releases) != len(tt.releases) { + t.Errorf("Expected %d releases, got %d", len(tt.releases), len(writer.releases)) + } + } + }) + } +} + +func TestReleaseListWriterMethods(t *testing.T) { + timestamp := time.Unix(1452902400, 0).UTC() + zeroTimestamp := time.Time{} + chartInfo := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "test-chart", + Version: "1.0.0", + AppVersion: "0.0.1", + }, + } + + releases := []*release.Release{ + { + Name: "test-release", + Version: 1, + Namespace: "default", + Info: &release.Info{ + LastDeployed: timestamp, + Status: common.StatusDeployed, + }, + Chart: chartInfo, + }, + { + Name: "zero-time-release", + Version: 1, + Namespace: "default", + Info: &release.Info{ + LastDeployed: zeroTimestamp, + Status: common.StatusFailed, + }, + Chart: chartInfo, + }, + } + + tests := []struct { + name string + status common.Status + }{ + {"deployed", common.StatusDeployed}, + {"failed", common.StatusFailed}, + {"pending-install", common.StatusPendingInstall}, + {"pending-upgrade", common.StatusPendingUpgrade}, + {"pending-rollback", common.StatusPendingRollback}, + {"uninstalling", common.StatusUninstalling}, + {"uninstalled", common.StatusUninstalled}, + {"superseded", common.StatusSuperseded}, + {"unknown", common.StatusUnknown}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testReleases := []*release.Release{ + { + Name: "test-release", + Version: 1, + Namespace: "default", + Info: &release.Info{ + LastDeployed: timestamp, + Status: tt.status, + }, + Chart: chartInfo, + }, + } + + writer := newReleaseListWriter(testReleases, "", false, false) + + var buf []byte + out := &bytesWriter{buf: &buf} + + err := writer.WriteJSON(out) + if err != nil { + t.Errorf("WriteJSON failed: %v", err) + } + + err = writer.WriteYAML(out) + if err != nil { + t.Errorf("WriteYAML failed: %v", err) + } + + err = writer.WriteTable(out) + if err != nil { + t.Errorf("WriteTable failed: %v", err) + } + }) + } + + writer := newReleaseListWriter(releases, "", false, false) + + var buf []byte + out := &bytesWriter{buf: &buf} + + err := writer.WriteJSON(out) + if err != nil { + t.Errorf("WriteJSON failed: %v", err) + } + + err = writer.WriteYAML(out) + if err != nil { + t.Errorf("WriteYAML failed: %v", err) + } + + err = writer.WriteTable(out) + if err != nil { + t.Errorf("WriteTable failed: %v", err) + } +} + +func TestFilterReleases(t *testing.T) { + releases := []*release.Release{ + {Name: "release1"}, + {Name: "release2"}, + {Name: "release3"}, + } + + tests := []struct { + name string + releases []*release.Release + ignoredReleaseNames []string + expectedCount int + }{ + { + name: "nil ignored list", + releases: releases, + ignoredReleaseNames: nil, + expectedCount: 3, + }, + { + name: "empty ignored list", + releases: releases, + ignoredReleaseNames: []string{}, + expectedCount: 3, + }, + { + name: "filter one release", + releases: releases, + ignoredReleaseNames: []string{"release1"}, + expectedCount: 2, + }, + { + name: "filter multiple releases", + releases: releases, + ignoredReleaseNames: []string{"release1", "release3"}, + expectedCount: 1, + }, + { + name: "filter non-existent release", + releases: releases, + ignoredReleaseNames: []string{"non-existent"}, + expectedCount: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := filterReleases(tt.releases, tt.ignoredReleaseNames) + if len(result) != tt.expectedCount { + t.Errorf("Expected %d releases, got %d", tt.expectedCount, len(result)) + } + }) + } +} + +type bytesWriter struct { + buf *[]byte +} + +func (b *bytesWriter) Write(p []byte) (n int, err error) { + *b.buf = append(*b.buf, p...) + return len(p), nil +} + +func TestListCustomTimeFormat(t *testing.T) { + defaultNamespace := "default" + timestamp := time.Unix(1452902400, 0).UTC() + chartInfo := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "test-chart", + Version: "1.0.0", + AppVersion: "0.0.1", + }, + } + + releaseFixture := []*release.Release{ + { + Name: "test-release", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp, + Status: common.StatusDeployed, + }, + Chart: chartInfo, + }, + } + + tests := []cmdTestCase{{ + name: "list releases with custom time format", + cmd: "list --time-format '2006-01-02 15:04:05'", + golden: "output/list-time-format.txt", + rels: releaseFixture, + }} + runTestCmd(t, tests) +} + +func TestListStatusMapping(t *testing.T) { + defaultNamespace := "default" + timestamp := time.Unix(1452902400, 0).UTC() + chartInfo := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "test-chart", + Version: "1.0.0", + AppVersion: "0.0.1", + }, + } + + testCases := []struct { + name string + status common.Status + }{ + {"deployed", common.StatusDeployed}, + {"failed", common.StatusFailed}, + {"pending-install", common.StatusPendingInstall}, + {"pending-upgrade", common.StatusPendingUpgrade}, + {"pending-rollback", common.StatusPendingRollback}, + {"uninstalling", common.StatusUninstalling}, + {"uninstalled", common.StatusUninstalled}, + {"superseded", common.StatusSuperseded}, + {"unknown", common.StatusUnknown}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + releaseFixture := []*release.Release{ + { + Name: "test-release", + Version: 1, + Namespace: defaultNamespace, + Info: &release.Info{ + LastDeployed: timestamp, + Status: tc.status, + }, + Chart: chartInfo, + }, + } + + writer := newReleaseListWriter(releaseFixture, "", false, false) + if len(writer.releases) != 1 { + t.Errorf("Expected 1 release, got %d", len(writer.releases)) + } + + if writer.releases[0].Status != tc.status.String() { + t.Errorf("Expected status %s, got %s", tc.status.String(), writer.releases[0].Status) + } + }) + } +} diff --git a/pkg/cmd/testdata/output/list-json.txt b/pkg/cmd/testdata/output/list-json.txt new file mode 100644 index 000000000..89e4d9dcf --- /dev/null +++ b/pkg/cmd/testdata/output/list-json.txt @@ -0,0 +1 @@ +[{"name":"test-release","namespace":"default","revision":"1","updated":"2016-01-16 00:00:00 +0000 UTC","status":"deployed","chart":"test-chart-1.0.0","app_version":"0.0.1"}] diff --git a/pkg/cmd/testdata/output/list-time-format.txt b/pkg/cmd/testdata/output/list-time-format.txt new file mode 100644 index 000000000..4d493da7c --- /dev/null +++ b/pkg/cmd/testdata/output/list-time-format.txt @@ -0,0 +1,2 @@ +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +test-release default 1 2016-01-16 00:00:00 deployed test-chart-1.0.0 0.0.1 diff --git a/pkg/cmd/testdata/output/list-yaml.txt b/pkg/cmd/testdata/output/list-yaml.txt new file mode 100644 index 000000000..9e1d41f30 --- /dev/null +++ b/pkg/cmd/testdata/output/list-yaml.txt @@ -0,0 +1,7 @@ +- app_version: 0.0.1 + chart: test-chart-1.0.0 + name: test-release + namespace: default + revision: "1" + status: deployed + updated: 2016-01-16 00:00:00 +0000 UTC From 3b2fd1779974122a66a3c2a382513fe7e7f5525a Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 13 Oct 2025 16:43:18 -0600 Subject: [PATCH 093/118] chore: rename test registry Signed-off-by: Terry Howe --- pkg/registry/client_http_test.go | 12 ++++++------ pkg/registry/client_insecure_tls_test.go | 12 ++++++------ pkg/registry/client_tls_test.go | 12 ++++++------ pkg/registry/{utils_test.go => registry_test.go} | 12 ++++++------ 4 files changed, 24 insertions(+), 24 deletions(-) rename pkg/registry/{utils_test.go => registry_test.go} (98%) diff --git a/pkg/registry/client_http_test.go b/pkg/registry/client_http_test.go index dddd29ee9..c4f950214 100644 --- a/pkg/registry/client_http_test.go +++ b/pkg/registry/client_http_test.go @@ -27,16 +27,16 @@ import ( ) type HTTPRegistryClientTestSuite struct { - TestSuite + TestRegistry } func (suite *HTTPRegistryClientTestSuite) SetupSuite() { // init test client - setup(&suite.TestSuite, false, false) + setup(&suite.TestRegistry, false, false) } func (suite *HTTPRegistryClientTestSuite) TearDownSuite() { - teardown(&suite.TestSuite) + teardown(&suite.TestRegistry) os.RemoveAll(suite.WorkspaceDir) } @@ -53,15 +53,15 @@ func (suite *HTTPRegistryClientTestSuite) Test_0_Login() { } func (suite *HTTPRegistryClientTestSuite) Test_1_Push() { - testPush(&suite.TestSuite) + testPush(&suite.TestRegistry) } func (suite *HTTPRegistryClientTestSuite) Test_2_Pull() { - testPull(&suite.TestSuite) + testPull(&suite.TestRegistry) } func (suite *HTTPRegistryClientTestSuite) Test_3_Tags() { - testTags(&suite.TestSuite) + testTags(&suite.TestRegistry) } func (suite *HTTPRegistryClientTestSuite) Test_4_ManInTheMiddle() { diff --git a/pkg/registry/client_insecure_tls_test.go b/pkg/registry/client_insecure_tls_test.go index 03354475a..e7f53c628 100644 --- a/pkg/registry/client_insecure_tls_test.go +++ b/pkg/registry/client_insecure_tls_test.go @@ -24,16 +24,16 @@ import ( ) type InsecureTLSRegistryClientTestSuite struct { - TestSuite + TestRegistry } func (suite *InsecureTLSRegistryClientTestSuite) SetupSuite() { // init test client - setup(&suite.TestSuite, true, true) + setup(&suite.TestRegistry, true, true) } func (suite *InsecureTLSRegistryClientTestSuite) TearDownSuite() { - teardown(&suite.TestSuite) + teardown(&suite.TestRegistry) os.RemoveAll(suite.WorkspaceDir) } @@ -50,15 +50,15 @@ func (suite *InsecureTLSRegistryClientTestSuite) Test_0_Login() { } func (suite *InsecureTLSRegistryClientTestSuite) Test_1_Push() { - testPush(&suite.TestSuite) + testPush(&suite.TestRegistry) } func (suite *InsecureTLSRegistryClientTestSuite) Test_2_Pull() { - testPull(&suite.TestSuite) + testPull(&suite.TestRegistry) } func (suite *InsecureTLSRegistryClientTestSuite) Test_3_Tags() { - testTags(&suite.TestSuite) + testTags(&suite.TestRegistry) } func (suite *InsecureTLSRegistryClientTestSuite) Test_4_Logout() { diff --git a/pkg/registry/client_tls_test.go b/pkg/registry/client_tls_test.go index 2bf1750a9..e7f00168b 100644 --- a/pkg/registry/client_tls_test.go +++ b/pkg/registry/client_tls_test.go @@ -26,16 +26,16 @@ import ( ) type TLSRegistryClientTestSuite struct { - TestSuite + TestRegistry } func (suite *TLSRegistryClientTestSuite) SetupSuite() { // init test client - setup(&suite.TestSuite, true, false) + setup(&suite.TestRegistry, true, false) } func (suite *TLSRegistryClientTestSuite) TearDownSuite() { - teardown(&suite.TestSuite) + teardown(&suite.TestRegistry) os.RemoveAll(suite.WorkspaceDir) } @@ -76,15 +76,15 @@ func (suite *TLSRegistryClientTestSuite) Test_1_Login() { } func (suite *TLSRegistryClientTestSuite) Test_1_Push() { - testPush(&suite.TestSuite) + testPush(&suite.TestRegistry) } func (suite *TLSRegistryClientTestSuite) Test_2_Pull() { - testPull(&suite.TestSuite) + testPull(&suite.TestRegistry) } func (suite *TLSRegistryClientTestSuite) Test_3_Tags() { - testTags(&suite.TestSuite) + testTags(&suite.TestRegistry) } func (suite *TLSRegistryClientTestSuite) Test_4_Logout() { diff --git a/pkg/registry/utils_test.go b/pkg/registry/registry_test.go similarity index 98% rename from pkg/registry/utils_test.go rename to pkg/registry/registry_test.go index 501860e03..4b0e72319 100644 --- a/pkg/registry/utils_test.go +++ b/pkg/registry/registry_test.go @@ -56,7 +56,7 @@ var ( testPassword = "mypass" ) -type TestSuite struct { +type TestRegistry struct { suite.Suite Out io.Writer DockerRegistryHost string @@ -66,7 +66,7 @@ type TestSuite struct { dockerRegistry *registry.Registry } -func setup(suite *TestSuite, tlsEnabled, insecure bool) { +func setup(suite *TestRegistry, tlsEnabled, insecure bool) { suite.WorkspaceDir = testWorkspaceDir os.RemoveAll(suite.WorkspaceDir) os.Mkdir(suite.WorkspaceDir, 0700) @@ -164,7 +164,7 @@ func setup(suite *TestSuite, tlsEnabled, insecure bool) { }() } -func teardown(suite *TestSuite) { +func teardown(suite *TestRegistry) { if suite.dockerRegistry != nil { _ = suite.dockerRegistry.Shutdown(context.Background()) } @@ -208,7 +208,7 @@ func initCompromisedRegistryTestServer() string { return fmt.Sprintf("localhost:%s", u.Port()) } -func testPush(suite *TestSuite) { +func testPush(suite *TestRegistry) { testingChartCreationTime := "1977-09-02T22:04:05Z" @@ -295,7 +295,7 @@ func testPush(suite *TestSuite) { result.Prov.Digest) } -func testPull(suite *TestSuite) { +func testPull(suite *TestRegistry) { // bad/missing ref ref := fmt.Sprintf("%s/testrepo/no-existy:1.2.3", suite.DockerRegistryHost) _, err := suite.RegistryClient.Pull(ref) @@ -374,7 +374,7 @@ func testPull(suite *TestSuite) { suite.Equal(provData, result.Prov.Data) } -func testTags(suite *TestSuite) { +func testTags(suite *TestRegistry) { // Load test chart (to build ref pushed in previous test) chartData, err := os.ReadFile("../downloader/testdata/local-subchart-0.1.0.tgz") suite.Nil(err, "no error loading test chart") From 4edf05096014f0cc67155917e2344871c41c1f35 Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Mon, 13 Oct 2025 17:05:23 -0600 Subject: [PATCH 094/118] feature: create copilot structured context Signed-off-by: Terry Howe --- .github/copilot-instructions.md | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..5232bbc82 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,48 @@ +# Copilot Instructions for Helm + +## Overview +Helm is a package manager for Kubernetes written in Go, supporting v3 (stable) and v4 (unstable) APIs. + +## Build & Test +```bash +make build # Build binary +make test # Run all tests (style + unit) +make test-unit # Unit tests only +make test-coverage # With coverage +make test-style # Linting +golangci-lint run # Direct linting +go test -run TestName # Specific test +``` + +## Code Structure +- `/cmd/helm/` - CLI entry point (Cobra-based) +- `/pkg/` - Public API + - `action/` - Core operations (install, upgrade, rollback) + - `chart/v2/` - Stable chart format + - `engine/` - Template rendering (Go templates + Sprig) + - `registry/` - OCI support + - `storage/` - Release backends (Secrets/ConfigMaps/SQL) +- `/internal/` - Private implementation + - `chart/v3/` - Next-gen chart format + +## Development Guidelines + +### Code Standards +- Use table-driven tests with testify +- Golden files in `testdata/` for complex output +- Mock Kubernetes clients for action tests +- All commits must include DCO sign-off: `git commit -s` + +### Branching +- `main` - Helm v4 development +- `dev-v3` - Helm v3 stable (backport from main) + +### Dependencies +- `k8s.io/client-go` - Kubernetes interaction +- `github.com/spf13/cobra` - CLI framework +- `github.com/Masterminds/sprig` - Template functions + +### Key Patterns +- **Actions**: Operations in `/pkg/action/` use shared Configuration +- **Dual Chart Support**: v2 (stable) in `/pkg/`, v3 (dev) in `/internal/` +- **Storage Abstraction**: Pluggable release storage backends From 054eabddd7c480e4dee6dce5db837c41ee19b9eb Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Tue, 14 Oct 2025 19:07:54 +0200 Subject: [PATCH 095/118] Return errors on upgrade when deletion fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a rebase of https://github.com/helm/helm/pull/12299 as the pull request was tagged for Helm v4. Closes: https://github.com/helm/helm/issues/11375 Related: https://github.com/helm/helm/pull/7929 It was a pain to reproduce, here is a script: ``` set -u NS=default RELEASE=test-release CHART=./test-chart SA=limited-helm-sa HELM=${HELM:-./bin/helm} echo "Helm: $($HELM version)" echo "Cleaning…" $HELM uninstall "$RELEASE" -n "$NS" >/dev/null 2>&1 || true kubectl -n "$NS" delete sa "$SA" role "${SA}-role" rolebinding "${SA}-rb" >/dev/null 2>&1 || true kubectl -n "$NS" delete cronjob "$RELEASE-test-chart-cronjob" >/dev/null 2>&1 || true rm -rf "$CHART" /tmp/limited-helm-kubeconfig echo "Create minimal chart with only a CronJob" $HELM create "$CHART" >/dev/null rm -f "$CHART"/templates/{deployment.yaml,service.yaml,hpa.yaml,tests/test-connection.yaml,serviceaccount.yaml} cat > "$CHART/templates/cronjob.yaml" <<'YAML' apiVersion: batch/v1 kind: CronJob metadata: name: {{ include "test-chart.fullname" . }}-cronjob spec: schedule: "*/5 * * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: hello image: busybox command: ["/bin/sh","-c","date; echo Hello from CronJob"] YAML echo "RBAC: allow Helm storage, basic reads/creates, but NO delete on cronjobs" kubectl -n "$NS" apply -f - >/dev/null </dev/null < "$KCFG" < "$CHART/templates/configmap.yaml" <<'YAML' apiVersion: v1 kind: ConfigMap metadata: name: {{ include "test-chart.fullname" . }}-config data: hello: world YAML echo "Upgrade without CronJob (as limited SA)" KUBECONFIG="$KCFG" $HELM upgrade --install "$RELEASE" "$CHART" -n "$NS" RC=$? echo "Post-upgrade verification" if kubectl -n "$NS" get cronjob "$RELEASE-test-chart-cronjob" >/dev/null 2>&1; then echo "OK: Stale CronJob still present: $RELEASE-test-chart-cronjob" else echo "NO_OK: CronJob deleted" fi echo "Helm exit code: $RC" exit 0 ``` With the current build: ```sh ./reproduce-helm-issue.sh Helm: version.BuildInfo{Version:"v4.0+unreleased", GitCommit:"f19bb9cd4c99943f7a4980d6670de44affe3e472", GitTreeState:"dirty", GoVersion:"go1.24.0"} Cleaning… Create minimal chart with CronJob + ConfigMap (we will remove both in v2) RBAC: allow Helm storage + delete for configmaps, but NO delete on cronjobs Create kubeconfig for that SA Install v1 (as limited SA) Release "test-release" does not exist. Installing it now. NAME: test-release LAST DEPLOYED: Tue Oct 14 18:55:57 2025 NAMESPACE: default STATUS: deployed REVISION: 1 DESCRIPTION: Install complete TEST SUITE: None NOTES: 1. Get the application URL by running these commands: export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=test-chart,app.kubernetes.io/instance=test-release" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT Verify v1 objects exist NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE test-release-test-chart-cronjob */5 * * * * False 0 0s NAME DATA AGE test-release-test-chart-config 1 0s Prepare v2: remove BOTH CronJob and ConfigMap from the chart Upgrade to v2 (as limited SA) — expecting CronJob delete first, then ConfigMap - CronJob delete should FAIL (no delete permission) - ConfigMap delete should SUCCEED (delete allowed) — proves 'continue on error' and inverted order level=DEBUG msg="getting history for release" release=test-release level=DEBUG msg="getting release history" name=test-release level=DEBUG msg="preparing upgrade" name=test-release level=DEBUG msg="getting last revision" name=test-release level=DEBUG msg="getting release history" name=test-release level=DEBUG msg="number of dependencies in the chart" dependencies=0 level=DEBUG msg="determined release apply method" server_side_apply=true previous_release_apply_method=ssa level=DEBUG msg="performing update" name=test-release level=DEBUG msg="creating upgraded release" name=test-release level=DEBUG msg="creating release" key=sh.helm.release.v1.test-release.v2 level=DEBUG msg="getting release history" name=test-release level=DEBUG msg="using server-side apply for resource update" forceConflicts=false dryRun=false fieldValidationDirective=Strict upgradeClientSideFieldManager=false level=DEBUG msg="checking resources for changes" resources=0 level=DEBUG msg="deleting resource" namespace=default name=test-release-test-chart-config kind=ConfigMap level=DEBUG msg="deleting resource" namespace=default name=test-release-test-chart-cronjob kind=CronJob level=DEBUG msg="failed to delete resource" namespace=default name=test-release-test-chart-cronjob kind=CronJob error="cronjobs.batch \"test-release-test-chart-cronjob\" is forbidden: User \"system:serviceaccount:default:limited-helm-sa\" cannot delete resource \"cronjobs\" in API group \"batch\" in the namespace \"default\"" level=INFO msg="update completed" created=0 updated=0 deleted=1 level=WARN msg="update completed with errors" errors=1 level=DEBUG msg="updating release" key=sh.helm.release.v1.test-release.v1 level=WARN msg="upgrade failed" name=test-release error="failed to delete resource test-release-test-chart-cronjob: cronjobs.batch \"test-release-test-chart-cronjob\" is forbidden: User \"system:serviceaccount:default:limited-helm-sa\" cannot delete resource \"cronjobs\" in API group \"batch\" in the namespace \"default\"" level=DEBUG msg="updating release" key=sh.helm.release.v1.test-release.v2 Error: UPGRADE FAILED: failed to delete resource test-release-test-chart-cronjob: cronjobs.batch "test-release-test-chart-cronjob" is forbidden: User "system:serviceaccount:default:limited-helm-sa" cannot delete resource "cronjobs" in API group "batch" in the namespace "default" Post-upgrade verification Stale CronJob still present: test-release-test-chart-cronjob (expected if delete is forbidden) ConfigMap deleted as expected: test-release-test-chart-config (and after CronJob attempt) Helm exit code: 1 ``` With last version v3.19: ``` HELM=/usr/local/bin/helm ./reproduce-helm-issue.sh Helm: version.BuildInfo{Version:"v3.19.0", GitCommit:"3d8990f0836691f0229297773f3524598f46bda6", GitTreeState:"clean", GoVersion:"go1.24.7"} Cleaning… Create minimal chart with only a CronJob RBAC: allow Helm storage, basic reads/creates, but NO delete on cronjobs Create kubeconfig for that SA Install (as limited SA) Release "test-release" does not exist. Installing it now. NAME: test-release LAST DEPLOYED: Tue Oct 14 19:07:01 2025 NAMESPACE: default STATUS: deployed REVISION: 1 TEST SUITE: None NOTES: 1. Get the application URL by running these commands: export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=test-chart,app.kubernetes.io/instance=test-release" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT CronJob after install: NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE test-release-test-chart-cronjob */5 * * * * False 0 0s Remove CronJob from chart and add a small ConfigMap to force an upgrade Upgrade without CronJob (as limited SA) Release "test-release" has been upgraded. Happy Helming! NAME: test-release LAST DEPLOYED: Tue Oct 14 19:07:01 2025 NAMESPACE: default STATUS: deployed REVISION: 2 TEST SUITE: None NOTES: 1. Get the application URL by running these commands: export POD_NAME=$(kubectl get pods --namespace default -l "app.kubernetes.io/name=test-chart,app.kubernetes.io/instance=test-release" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace default $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace default port-forward $POD_NAME 8080:$CONTAINER_PORT Post-upgrade verification OK: Stale CronJob still present: test-release-test-chart-cronjob Helm exit code: 0 ``` Co-authored-by: dayeguilaiye <979014041@qq.com> Signed-off-by: Benoit Tigeot --- pkg/kube/client.go | 7 +++++++ pkg/kube/client_test.go | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 30c014ad5..bc9ee3cc3 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -569,10 +569,17 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA } if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil { slog.Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + if !apierrors.IsNotFound(err) { + updateErrors = append(updateErrors, fmt.Errorf("failed to delete resource %s: %w", info.Name, err)) + } continue } res.Deleted = append(res.Deleted, info) } + + if len(updateErrors) != 0 { + return res, joinErrors(updateErrors, " && ") + } return res, nil } diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index d8c0fba5f..c6845fc52 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -336,6 +336,8 @@ func TestUpdate(t *testing.T) { "/namespaces/default/pods:POST", // retry due to 409 "/namespaces/default/pods/squid:GET", "/namespaces/default/pods/squid:DELETE", + "/namespaces/default/pods/notfound:GET", + "/namespaces/default/pods/notfound:DELETE", } expectedActionsServerSideApply := []string{ @@ -351,11 +353,13 @@ func TestUpdate(t *testing.T) { "/namespaces/default/pods:POST", // retry due to 409 "/namespaces/default/pods/squid:GET", "/namespaces/default/pods/squid:DELETE", + "/namespaces/default/pods/notfound:GET", + "/namespaces/default/pods/notfound:DELETE", } testCases := map[string]testCase{ "client-side apply": { - OriginalPods: newPodList("starfish", "otter", "squid"), + OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), TargetPods: func() v1.PodList { listTarget := newPodList("starfish", "otter", "dolphin") listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} @@ -367,7 +371,7 @@ func TestUpdate(t *testing.T) { ExpectedActions: expectedActionsClientSideApply, }, "client-side apply (three-way merge for unstructured)": { - OriginalPods: newPodList("starfish", "otter", "squid"), + OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), TargetPods: func() v1.PodList { listTarget := newPodList("starfish", "otter", "dolphin") listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} @@ -379,7 +383,7 @@ func TestUpdate(t *testing.T) { ExpectedActions: expectedActionsClientSideApply, }, "serverSideApply": { - OriginalPods: newPodList("starfish", "otter", "squid"), + OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), TargetPods: func() v1.PodList { listTarget := newPodList("starfish", "otter", "dolphin") listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} @@ -444,6 +448,12 @@ func TestUpdate(t *testing.T) { return newResponse(http.StatusOK, &listTarget.Items[1]) case p == "/namespaces/default/pods/squid" && m == http.MethodGet: return newResponse(http.StatusOK, &listTarget.Items[2]) + case p == "/namespaces/default/pods/notfound" && m == http.MethodGet: + // Resource exists in original but will simulate not found on delete + return newResponse(http.StatusOK, &listOriginal.Items[3]) + case p == "/namespaces/default/pods/notfound" && m == http.MethodDelete: + // Simulate a not found during deletion; should not cause update to fail + return newResponse(http.StatusNotFound, notFoundBody()) default: } From 7097c8e2e5cdaa3af7a93b040669d66894f0bfd0 Mon Sep 17 00:00:00 2001 From: Benoit Tigeot Date: Wed, 15 Oct 2025 00:32:35 +0200 Subject: [PATCH 096/118] Replicate as unit test case where we fail once a resource deletion Signed-off-by: Benoit Tigeot --- pkg/kube/client_test.go | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index c6845fc52..3934171be 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -321,6 +321,7 @@ func TestUpdate(t *testing.T) { ThreeWayMergeForUnstructured bool ServerSideApply bool ExpectedActions []string + ExpectedError string } expectedActionsClientSideApply := []string{ @@ -369,6 +370,7 @@ func TestUpdate(t *testing.T) { ThreeWayMergeForUnstructured: false, ServerSideApply: false, ExpectedActions: expectedActionsClientSideApply, + ExpectedError: "", }, "client-side apply (three-way merge for unstructured)": { OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), @@ -381,6 +383,7 @@ func TestUpdate(t *testing.T) { ThreeWayMergeForUnstructured: true, ServerSideApply: false, ExpectedActions: expectedActionsClientSideApply, + ExpectedError: "", }, "serverSideApply": { OriginalPods: newPodList("starfish", "otter", "squid", "notfound"), @@ -393,6 +396,23 @@ func TestUpdate(t *testing.T) { ThreeWayMergeForUnstructured: false, ServerSideApply: true, ExpectedActions: expectedActionsServerSideApply, + ExpectedError: "", + }, + "serverSideApply with forbidden deletion": { + OriginalPods: newPodList("starfish", "otter", "squid", "notfound", "forbidden"), + TargetPods: func() v1.PodList { + listTarget := newPodList("starfish", "otter", "dolphin") + listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} + + return listTarget + }(), + ThreeWayMergeForUnstructured: false, + ServerSideApply: true, + ExpectedActions: append(expectedActionsServerSideApply, + "/namespaces/default/pods/forbidden:GET", + "/namespaces/default/pods/forbidden:DELETE", + ), + ExpectedError: "failed to delete resource forbidden:", }, } @@ -454,6 +474,16 @@ func TestUpdate(t *testing.T) { case p == "/namespaces/default/pods/notfound" && m == http.MethodDelete: // Simulate a not found during deletion; should not cause update to fail return newResponse(http.StatusNotFound, notFoundBody()) + case p == "/namespaces/default/pods/forbidden" && m == http.MethodGet: + return newResponse(http.StatusOK, &listOriginal.Items[4]) + case p == "/namespaces/default/pods/forbidden" && m == http.MethodDelete: + // Simulate RBAC forbidden that should cause update to fail + return newResponse(http.StatusForbidden, &metav1.Status{ + Status: metav1.StatusFailure, + Message: "pods \"forbidden\" is forbidden: User \"test-user\" cannot delete resource \"pods\" in API group \"\" in the namespace \"default\"", + Reason: metav1.StatusReasonForbidden, + Code: http.StatusForbidden, + }) default: } @@ -481,7 +511,13 @@ func TestUpdate(t *testing.T) { ClientUpdateOptionForceReplace(false), ClientUpdateOptionServerSideApply(tc.ServerSideApply, false), ClientUpdateOptionUpgradeClientSideFieldManager(true)) - require.NoError(t, err) + + if tc.ExpectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.ExpectedError) + } else { + require.NoError(t, err) + } assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated)) From b9eadd3186023fafe13421c9bd10ee4cc0883bca Mon Sep 17 00:00:00 2001 From: wyrapeseed Date: Wed, 15 Oct 2025 11:19:41 +0800 Subject: [PATCH 097/118] chore: fix some comment format Signed-off-by: wyrapeseed --- internal/plugin/installer/extractor.go | 4 ++-- internal/plugin/runtime_subprocess_test.go | 2 +- internal/sympath/walk.go | 2 +- pkg/cmd/search_repo.go | 2 +- pkg/engine/files.go | 2 +- pkg/getter/httpgetter_test.go | 4 ++-- pkg/getter/plugingetter.go | 2 +- pkg/strvals/parser.go | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/plugin/installer/extractor.go b/internal/plugin/installer/extractor.go index 407138197..71efebc67 100644 --- a/internal/plugin/installer/extractor.go +++ b/internal/plugin/installer/extractor.go @@ -85,10 +85,10 @@ func NewExtractor(source string) (Extractor, error) { // // - The character `:` is considered illegal because it is a separator on UNIX and a // drive designator on Windows. -// - The path component `..` is considered suspicions, and therefore illegal +// - The path component `..` is considered suspicious, and therefore illegal // - The character \ (backslash) is treated as a path separator and is converted to /. // - Beginning a path with a path separator is illegal -// - Rudimentary symlink protects are offered by SecureJoin. +// - Rudimentary symlink protections are offered by SecureJoin. func cleanJoin(root, dest string) (string, error) { // On Windows, this is a drive separator. On UNIX-like, this is the path list separator. diff --git a/internal/plugin/runtime_subprocess_test.go b/internal/plugin/runtime_subprocess_test.go index 243f4ad7c..ed251d28b 100644 --- a/internal/plugin/runtime_subprocess_test.go +++ b/internal/plugin/runtime_subprocess_test.go @@ -71,7 +71,7 @@ func TestSubprocessPluginRuntime(t *testing.T) { output, err := p.Invoke(t.Context(), &Input{ Message: schema.InputMessageCLIV1{ ExtraArgs: []string{"arg1", "arg2"}, - //Env: []string{"FOO=bar"}, + // Env: []string{"FOO=bar"}, }, }) diff --git a/internal/sympath/walk.go b/internal/sympath/walk.go index f67b9f1b9..812bb68ce 100644 --- a/internal/sympath/walk.go +++ b/internal/sympath/walk.go @@ -70,7 +70,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error { if err != nil { return fmt.Errorf("error evaluating symlink %s: %w", path, err) } - //This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons. + // This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons. slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved) if info, err = os.Lstat(resolved); err != nil { return err diff --git a/pkg/cmd/search_repo.go b/pkg/cmd/search_repo.go index 35608e22e..07345a48f 100644 --- a/pkg/cmd/search_repo.go +++ b/pkg/cmd/search_repo.go @@ -287,7 +287,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string { if isNotExist(err) { // If there is no cached charts file, fallback to the full index file. // This is much slower but can happen after the caching feature is first - // installed but before the user does a 'helm repo update' to generate the + // installed but before the user does a 'helm repo update' to generate the // first cached charts file. path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) if indexFile, err := repo.LoadIndexFile(path); err == nil { diff --git a/pkg/engine/files.go b/pkg/engine/files.go index f0a86988e..7834cac2c 100644 --- a/pkg/engine/files.go +++ b/pkg/engine/files.go @@ -64,7 +64,7 @@ func (f files) Get(name string) string { } // Glob takes a glob pattern and returns another files object only containing -// matched files. +// matched files. // // This is designed to be called from a template. // diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index f87d71877..96bfa1ece 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -520,11 +520,11 @@ func TestHTTPGetterTarDownload(t *testing.T) { b := make([]byte, 512) f.Read(b) - //Get the file size + // Get the file size FileStat, _ := f.Stat() FileSize := strconv.FormatInt(FileStat.Size(), 10) - //Simulating improper header values from bitbucket + // Simulating improper header values from bitbucket w.Header().Set("Content-Type", "application/x-tar") w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Length", FileSize) diff --git a/pkg/getter/plugingetter.go b/pkg/getter/plugingetter.go index 32dbc70c9..d74611637 100644 --- a/pkg/getter/plugingetter.go +++ b/pkg/getter/plugingetter.go @@ -109,7 +109,7 @@ func (g *getterPlugin) Get(href string, options ...Option) (*bytes.Buffer, error Protocol: u.Scheme, }, // TODO should we pass Stdin, Stdout, and Stderr through Input here to getter plugins? - //Stdout: os.Stdout, + // Stdout: os.Stdout, } output, err := g.plg.Invoke(context.Background(), input) if err != nil { diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index c65e98c84..86e349f37 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -237,7 +237,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e _, err = t.emptyVal() return err } - //End of key. Consume =, Get value. + // End of key. Consume =, Get value. // FIXME: Get value list first vl, e := t.valList() switch e { From 917822cfca6f146a893e386a788619065626810e Mon Sep 17 00:00:00 2001 From: Terry Howe Date: Fri, 17 Oct 2025 05:08:32 -0600 Subject: [PATCH 098/118] refactor: remove unused err from pkg/registry/client.go Signed-off-by: Terry Howe --- pkg/registry/client.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 95250f8da..1cb629657 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -75,11 +75,11 @@ type ( credentialsStore credentials.Store httpClient *http.Client plainHTTP bool - err error // pass any errors from the ClientOption functions } // ClientOption allows specifying various settings configurable by the user for overriding the defaults // used when creating a new default client + // TODO(TerryHowe): ClientOption should return error in v5 ClientOption func(*Client) ) @@ -90,9 +90,6 @@ func NewClient(options ...ClientOption) (*Client, error) { } for _, option := range options { option(client) - if client.err != nil { - return nil, client.err - } } if client.credentialsFile == "" { client.credentialsFile = helmpath.ConfigPath(CredentialsFileBasename) From 96b4c363c8491af8c0597b804abc27289354a11c Mon Sep 17 00:00:00 2001 From: Jesse Simpson Date: Fri, 17 Oct 2025 23:25:21 -0400 Subject: [PATCH 099/118] fix: Update returns nil on error, test should reflect this Signed-off-by: Jesse Simpson --- pkg/kube/fake/failing_kube_client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/kube/fake/failing_kube_client.go b/pkg/kube/fake/failing_kube_client.go index f340c045f..36f113814 100644 --- a/pkg/kube/fake/failing_kube_client.go +++ b/pkg/kube/fake/failing_kube_client.go @@ -122,7 +122,7 @@ func (f *FailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, d time. // Update returns the configured error if set or prints func (f *FailingKubeClient) Update(r, modified kube.ResourceList, options ...kube.ClientUpdateOption) (*kube.Result, error) { if f.UpdateError != nil { - return &kube.Result{}, f.UpdateError + return nil, f.UpdateError } return f.PrintingKubeClient.Update(r, modified, options...) } From 3f860e83fb55387e638a2245431a607b2b1e0579 Mon Sep 17 00:00:00 2001 From: Jesse Simpson Date: Sat, 18 Oct 2025 09:12:38 -0400 Subject: [PATCH 100/118] fix: use empty results instead of nil Signed-off-by: Jesse Simpson --- pkg/kube/client.go | 8 ++++---- pkg/kube/fake/failing_kube_client.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 30c014ad5..a6cba9ba9 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -689,19 +689,19 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate errs = append(errs, o(&updateOptions)) } if err := errors.Join(errs...); err != nil { - return nil, fmt.Errorf("invalid client update option(s): %w", err) + return &Result{}, fmt.Errorf("invalid client update option(s): %w", err) } if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply { - return nil, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together") + return &Result{}, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together") } if updateOptions.forceConflicts && updateOptions.forceReplace { - return nil, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together") + return &Result{}, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together") } if updateOptions.serverSideApply && updateOptions.forceReplace { - return nil, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together") + return &Result{}, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together") } makeUpdateApplyFunc := func() UpdateApplyFunc { diff --git a/pkg/kube/fake/failing_kube_client.go b/pkg/kube/fake/failing_kube_client.go index 36f113814..f340c045f 100644 --- a/pkg/kube/fake/failing_kube_client.go +++ b/pkg/kube/fake/failing_kube_client.go @@ -122,7 +122,7 @@ func (f *FailingKubeWaiter) WatchUntilReady(resources kube.ResourceList, d time. // Update returns the configured error if set or prints func (f *FailingKubeClient) Update(r, modified kube.ResourceList, options ...kube.ClientUpdateOption) (*kube.Result, error) { if f.UpdateError != nil { - return nil, f.UpdateError + return &kube.Result{}, f.UpdateError } return f.PrintingKubeClient.Update(r, modified, options...) } From b05411506a91360d5946b3ece768b016e98b79bc Mon Sep 17 00:00:00 2001 From: Andreas Sommer Date: Mon, 20 Oct 2025 15:23:34 +0200 Subject: [PATCH 101/118] Improve error message when plugin source cannot be determined or a non-directory is passed Signed-off-by: Andreas Sommer --- internal/plugin/installer/installer.go | 24 ++++++++++++------- internal/plugin/installer/local_installer.go | 6 ++--- .../plugin/installer/local_installer_test.go | 2 +- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/internal/plugin/installer/installer.go b/internal/plugin/installer/installer.go index a6599c443..c7c1a8801 100644 --- a/internal/plugin/installer/installer.go +++ b/internal/plugin/installer/installer.go @@ -139,18 +139,24 @@ func Update(i Installer) error { } // NewForSource determines the correct Installer for the given source. -func NewForSource(source, version string) (Installer, error) { - // Check if source is an OCI registry reference +func NewForSource(source, version string) (installer Installer, err error) { if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) { - return NewOCIInstaller(source) - } - // Check if source is a local directory - if isLocalReference(source) { - return NewLocalInstaller(source) + // Source is an OCI registry reference + installer, err = NewOCIInstaller(source) + } else if isLocalReference(source) { + // Source is a local directory + installer, err = NewLocalInstaller(source) } else if isRemoteHTTPArchive(source) { - return NewHTTPInstaller(source) + installer, err = NewHTTPInstaller(source) + } else { + installer, err = NewVCSInstaller(source, version) + } + + if err != nil { + return installer, fmt.Errorf("cannot get information about plugin source %q (if it's a local directory, does it exist?), last error was: %w", source, err) } - return NewVCSInstaller(source, version) + + return } // FindSource determines the correct Installer for the given source. diff --git a/internal/plugin/installer/local_installer.go b/internal/plugin/installer/local_installer.go index e02261d59..1c8314282 100644 --- a/internal/plugin/installer/local_installer.go +++ b/internal/plugin/installer/local_installer.go @@ -29,8 +29,8 @@ import ( "helm.sh/helm/v4/pkg/helmpath" ) -// ErrPluginNotAFolder indicates that the plugin path is not a folder. -var ErrPluginNotAFolder = errors.New("expected plugin to be a folder") +// ErrPluginNotADirectory indicates that the plugin path is not a directory. +var ErrPluginNotADirectory = errors.New("expected plugin to be a directory (containing a file 'plugin.yaml')") // LocalInstaller installs plugins from the filesystem. type LocalInstaller struct { @@ -91,7 +91,7 @@ func (i *LocalInstaller) installFromDirectory() error { return err } if !stat.IsDir() { - return ErrPluginNotAFolder + return ErrPluginNotADirectory } if !isPlugin(i.Source) { diff --git a/internal/plugin/installer/local_installer_test.go b/internal/plugin/installer/local_installer_test.go index 189108fdb..2decb695f 100644 --- a/internal/plugin/installer/local_installer_test.go +++ b/internal/plugin/installer/local_installer_test.go @@ -64,7 +64,7 @@ func TestLocalInstallerNotAFolder(t *testing.T) { if err == nil { t.Fatal("expected error") } - if err != ErrPluginNotAFolder { + if err != ErrPluginNotADirectory { t.Fatalf("expected error to equal: %q", err) } } From b1d4dc680dcf7d6c8c8213c16b7fc9daa80a9f53 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Tue, 21 Oct 2025 22:48:51 +0100 Subject: [PATCH 102/118] feat: reinstate logger parameter to actions package Fixes: #31399 Signed-off-by: Evans Mungai --- pkg/action/action.go | 41 +++++++++++++-- pkg/action/action_test.go | 2 +- pkg/action/history.go | 4 +- pkg/action/install.go | 22 ++++---- pkg/action/rollback.go | 23 ++++---- pkg/action/uninstall.go | 12 ++--- pkg/action/upgrade.go | 30 +++++------ pkg/cmd/root.go | 2 +- pkg/kube/client.go | 65 +++++++++++++++-------- pkg/storage/driver/cfgmaps.go | 44 ++++++++++++---- pkg/storage/driver/memory.go | 24 ++++++++- pkg/storage/driver/secrets.go | 28 ++++++++-- pkg/storage/driver/sql.go | 99 +++++++++++++++++++++-------------- pkg/storage/storage.go | 53 +++++++++++++------ 14 files changed, 306 insertions(+), 143 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index fd75b85d3..4641b7427 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -29,6 +29,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "text/template" "time" @@ -109,9 +110,19 @@ type Configuration struct { // HookOutputFunc called with container name and returns and expects writer that will receive the log output. HookOutputFunc func(namespace, pod, container string) io.Writer + // logger is an slog.Logger pointer to use with the Configuration instance + logger atomic.Pointer[slog.Logger] + + // Mutex is an exclusive lock for concurrent access to the action mutex sync.Mutex } +func NewConfiguration() *Configuration { + c := &Configuration{} + c.SetLogger(slog.Default()) + return c +} + const ( // filenameAnnotation is the annotation key used to store the original filename // information in manifest annotations for post-rendering reconstruction. @@ -376,8 +387,8 @@ func (cfg *Configuration) getCapabilities() (*common.Capabilities, error) { apiVersions, err := GetVersionSet(dc) if err != nil { if discovery.IsGroupDiscoveryFailedError(err) { - slog.Warn("the kubernetes server has an orphaned API service", slog.Any("error", err)) - slog.Warn("to fix this, kubectl delete apiservice ") + cfg.Logger().Warn("the kubernetes server has an orphaned API service", slog.Any("error", err)) + cfg.Logger().Warn("to fix this, kubectl delete apiservice ") } else { return nil, fmt.Errorf("could not get apiVersions from Kubernetes: %w", err) } @@ -476,13 +487,14 @@ func GetVersionSet(client discovery.ServerResourcesInterface) (common.VersionSet // recordRelease with an update operation in case reuse has been set. func (cfg *Configuration) recordRelease(r *release.Release) { if err := cfg.Releases.Update(r); err != nil { - slog.Warn("failed to update release", "name", r.Name, "revision", r.Version, slog.Any("error", err)) + cfg.Logger().Warn("failed to update release", "name", r.Name, "revision", r.Version, slog.Any("error", err)) } } // Init initializes the action configuration func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string) error { kc := kube.New(getter) + kc.SetLogger(cfg.Logger()) lazyClient := &lazyClient{ namespace: namespace, @@ -493,9 +505,11 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp switch helmDriver { case "secret", "secrets", "": d := driver.NewSecrets(newSecretClient(lazyClient)) + d.SetLogger(cfg.Logger()) store = storage.Init(d) case "configmap", "configmaps": d := driver.NewConfigMaps(newConfigMapClient(lazyClient)) + d.SetLogger(cfg.Logger()) store = storage.Init(d) case "memory": var d *driver.Memory @@ -510,11 +524,13 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp if d == nil { d = driver.NewMemory() } + d.SetLogger(cfg.Logger()) d.SetNamespace(namespace) store = storage.Init(d) case "sql": d, err := driver.NewSQL( os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"), + cfg.Logger(), namespace, ) if err != nil { @@ -528,6 +544,7 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp cfg.RESTClientGetter = getter cfg.KubeClient = kc cfg.Releases = store + cfg.Releases.SetLogger(cfg.Logger()) cfg.HookOutputFunc = func(_, _, _ string) io.Writer { return io.Discard } return nil @@ -538,6 +555,24 @@ func (cfg *Configuration) SetHookOutputFunc(hookOutputFunc func(_, _, _ string) cfg.HookOutputFunc = hookOutputFunc } +// Logger returns the logger for the Configuration. If nil, returns slog.Default(). +func (cfg *Configuration) Logger() *slog.Logger { + if lg := cfg.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +// SetLogger sets the logger for the Configuration. If nil, sets the default logger. +func (cfg *Configuration) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + cfg.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + cfg.logger.Store(newLogger) +} + func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod { if serverSideApply { return release.ApplyMethodServerSideApply diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 06329095e..8f9159dd1 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -344,7 +344,7 @@ func TestConfiguration_Init(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - cfg := &Configuration{} + cfg := NewConfiguration() actualErr := cfg.Init(nil, "default", tt.helmDriver) if tt.expectErr { diff --git a/pkg/action/history.go b/pkg/action/history.go index dc3ab51d4..90307c79b 100644 --- a/pkg/action/history.go +++ b/pkg/action/history.go @@ -17,8 +17,6 @@ limitations under the License. package action import ( - "log/slog" - "fmt" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -55,6 +53,6 @@ func (h *History) Run(name string) ([]release.Releaser, error) { return nil, fmt.Errorf("release name is invalid: %s", name) } - slog.Debug("getting history for release", "release", name) + h.cfg.Logger().Debug("getting history for release", "release", name) return h.cfg.Releases.History(name) } diff --git a/pkg/action/install.go b/pkg/action/install.go index e4b23aceb..433b9a8d8 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -193,7 +193,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error { // If the error is CRD already exists, continue. if apierrors.IsAlreadyExists(err) { crdName := res[0].Name - slog.Debug("CRD is already present. Skipping", "crd", crdName) + i.cfg.Logger().Debug("CRD is already present. Skipping", "crd", crdName) continue } return fmt.Errorf("failed to install CRD %s: %w", obj.Name, err) @@ -221,7 +221,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error { return err } - slog.Debug("clearing discovery cache") + i.cfg.Logger().Debug("clearing discovery cache") discoveryClient.Invalidate() _, _ = discoveryClient.ServerGroups() @@ -234,7 +234,7 @@ func (i *Install) installCRDs(crds []chart.CRD) error { return err } if resettable, ok := restMapper.(meta.ResettableRESTMapper); ok { - slog.Debug("clearing REST mapper cache") + i.cfg.Logger().Debug("clearing REST mapper cache") resettable.Reset() } } @@ -267,24 +267,24 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st if interactWithServer(i.DryRunStrategy) { if err := i.cfg.KubeClient.IsReachable(); err != nil { - slog.Error(fmt.Sprintf("cluster reachability check failed: %v", err)) + i.cfg.Logger().Error(fmt.Sprintf("cluster reachability check failed: %v", err)) return nil, fmt.Errorf("cluster reachability check failed: %w", err) } } // HideSecret must be used with dry run. Otherwise, return an error. if !isDryRun(i.DryRunStrategy) && i.HideSecret { - slog.Error("hiding Kubernetes secrets requires a dry-run mode") + i.cfg.Logger().Error("hiding Kubernetes secrets requires a dry-run mode") return nil, errors.New("hiding Kubernetes secrets requires a dry-run mode") } if err := i.availableName(); err != nil { - slog.Error("release name check failed", slog.Any("error", err)) + i.cfg.Logger().Error("release name check failed", slog.Any("error", err)) return nil, fmt.Errorf("release name check failed: %w", err) } if err := chartutil.ProcessDependencies(chrt, vals); err != nil { - slog.Error("chart dependencies processing failed", slog.Any("error", err)) + i.cfg.Logger().Error("chart dependencies processing failed", slog.Any("error", err)) return nil, fmt.Errorf("chart dependencies processing failed: %w", err) } @@ -293,7 +293,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st if crds := chrt.CRDObjects(); interactWithServer(i.DryRunStrategy) && !i.SkipCRDs && len(crds) > 0 { // On dry run, bail here if isDryRun(i.DryRunStrategy) { - slog.Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") + i.cfg.Logger().Warn("This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") } else if err := i.installCRDs(crds); err != nil { return nil, err } @@ -313,7 +313,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st mem.SetNamespace(i.Namespace) i.cfg.Releases = storage.Init(mem) } else if interactWithServer(i.DryRunStrategy) && len(i.APIVersions) > 0 { - slog.Debug("API Version list given outside of client only mode, this list will be ignored") + i.cfg.Logger().Debug("API Version list given outside of client only mode, this list will be ignored") } // Make sure if RollbackOnFailure is set, that wait is set as well. This makes it so @@ -539,7 +539,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource // One possible strategy would be to do a timed retry to see if we can get // this stored in the future. if err := i.recordRelease(rel); err != nil { - slog.Error("failed to record the release", slog.Any("error", err)) + i.cfg.Logger().Error("failed to record the release", slog.Any("error", err)) } return rel, nil @@ -548,7 +548,7 @@ func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.Resource func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { rel.SetStatus(rcommon.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) if i.RollbackOnFailure { - slog.Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName) + i.cfg.Logger().Debug("install failed and rollback-on-failure is set, uninstalling release", "release", i.ReleaseName) uninstall := NewUninstall(i.cfg) uninstall.DisableHooks = i.DisableHooks uninstall.KeepHistory = false diff --git a/pkg/action/rollback.go b/pkg/action/rollback.go index 992f6979f..f75ca2429 100644 --- a/pkg/action/rollback.go +++ b/pkg/action/rollback.go @@ -19,7 +19,6 @@ package action import ( "bytes" "fmt" - "log/slog" "strings" "time" @@ -76,26 +75,26 @@ func (r *Rollback) Run(name string) error { r.cfg.Releases.MaxHistory = r.MaxHistory - slog.Debug("preparing rollback", "name", name) + r.cfg.Logger().Debug("preparing rollback", "name", name) currentRelease, targetRelease, serverSideApply, err := r.prepareRollback(name) if err != nil { return err } if !isDryRun(r.DryRunStrategy) { - slog.Debug("creating rolled back release", "name", name) + r.cfg.Logger().Debug("creating rolled back release", "name", name) if err := r.cfg.Releases.Create(targetRelease); err != nil { return err } } - slog.Debug("performing rollback", "name", name) + r.cfg.Logger().Debug("performing rollback", "name", name) if _, err := r.performRollback(currentRelease, targetRelease, serverSideApply); err != nil { return err } if !isDryRun(r.DryRunStrategy) { - slog.Debug("updating status for rolled back release", "name", name) + r.cfg.Logger().Debug("updating status for rolled back release", "name", name) if err := r.cfg.Releases.Update(targetRelease); err != nil { return err } @@ -151,7 +150,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele return nil, nil, false, fmt.Errorf("release has no %d version", previousVersion) } - slog.Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion) + r.cfg.Logger().Debug("rolling back", "name", name, "currentVersion", currentRelease.Version, "targetVersion", previousVersion) previousReleasei, err := r.cfg.Releases.Get(name, previousVersion) if err != nil { @@ -194,7 +193,7 @@ func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Rele func (r *Rollback) performRollback(currentRelease, targetRelease *release.Release, serverSideApply bool) (*release.Release, error) { if isDryRun(r.DryRunStrategy) { - slog.Debug("dry run", "name", targetRelease.Name) + r.cfg.Logger().Debug("dry run", "name", targetRelease.Name) return targetRelease, nil } @@ -214,7 +213,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas return targetRelease, err } } else { - slog.Debug("rollback hooks disabled", "name", targetRelease.Name) + r.cfg.Logger().Debug("rollback hooks disabled", "name", targetRelease.Name) } // It is safe to use "forceOwnership" here because these are resources currently rendered by the chart. @@ -232,21 +231,21 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas if err != nil { msg := fmt.Sprintf("Rollback %q failed: %s", targetRelease.Name, err) - slog.Warn(msg) + r.cfg.Logger().Warn(msg) currentRelease.Info.Status = common.StatusSuperseded targetRelease.Info.Status = common.StatusFailed targetRelease.Info.Description = msg r.cfg.recordRelease(currentRelease) r.cfg.recordRelease(targetRelease) if r.CleanupOnFail { - slog.Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created)) + r.cfg.Logger().Debug("cleanup on fail set, cleaning up resources", "count", len(results.Created)) _, errs := r.cfg.KubeClient.Delete(results.Created, metav1.DeletePropagationBackground) if errs != nil { return targetRelease, fmt.Errorf( "an error occurred while cleaning up resources. original rollback error: %w", fmt.Errorf("unable to cleanup resources: %w", joinErrors(errs, ", "))) } - slog.Debug("resource cleanup complete") + r.cfg.Logger().Debug("resource cleanup complete") } return targetRelease, err } @@ -288,7 +287,7 @@ func (r *Rollback) performRollback(currentRelease, targetRelease *release.Releas if err != nil { return nil, err } - slog.Debug("superseding previous deployment", "version", rel.Version) + r.cfg.Logger().Debug("superseding previous deployment", "version", rel.Version) rel.Info.Status = common.StatusSuperseded r.cfg.recordRelease(rel) } diff --git a/pkg/action/uninstall.go b/pkg/action/uninstall.go index 4ce6068ec..0cb31be43 100644 --- a/pkg/action/uninstall.go +++ b/pkg/action/uninstall.go @@ -119,7 +119,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) return nil, fmt.Errorf("the release named %q is already deleted", name) } - slog.Debug("uninstall: deleting release", "name", name) + u.cfg.Logger().Debug("uninstall: deleting release", "name", name) rel.Info.Status = common.StatusUninstalling rel.Info.Deleted = time.Now() rel.Info.Description = "Deletion in progress (or silently failed)" @@ -131,18 +131,18 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) return res, err } } else { - slog.Debug("delete hooks disabled", "release", name) + u.cfg.Logger().Debug("delete hooks disabled", "release", name) } // From here on out, the release is currently considered to be in StatusUninstalling // state. if err := u.cfg.Releases.Update(rel); err != nil { - slog.Debug("uninstall: Failed to store updated release", slog.Any("error", err)) + u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err)) } deletedResources, kept, errs := u.deleteRelease(rel) if errs != nil { - slog.Debug("uninstall: Failed to delete release", slog.Any("error", errs)) + u.cfg.Logger().Debug("uninstall: Failed to delete release", slog.Any("error", errs)) return nil, fmt.Errorf("failed to delete release: %s", name) } @@ -170,7 +170,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) } if !u.KeepHistory { - slog.Debug("purge requested", "release", name) + u.cfg.Logger().Debug("purge requested", "release", name) err := u.purgeReleases(rels...) if err != nil { errs = append(errs, fmt.Errorf("uninstall: Failed to purge the release: %w", err)) @@ -185,7 +185,7 @@ func (u *Uninstall) Run(name string) (*releasei.UninstallReleaseResponse, error) } if err := u.cfg.Releases.Update(rel); err != nil { - slog.Debug("uninstall: Failed to store updated release", slog.Any("error", err)) + u.cfg.Logger().Debug("uninstall: Failed to store updated release", slog.Any("error", err)) } if len(errs) > 0 { diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index 932743ce9..aded99dca 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -185,7 +185,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char return nil, fmt.Errorf("release name is invalid: %s", name) } - slog.Debug("preparing upgrade", "name", name) + u.cfg.Logger().Debug("preparing upgrade", "name", name) currentRelease, upgradedRelease, serverSideApply, err := u.prepareUpgrade(name, chrt, vals) if err != nil { return nil, err @@ -193,7 +193,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char u.cfg.Releases.MaxHistory = u.MaxHistory - slog.Debug("performing update", "name", name) + u.cfg.Logger().Debug("performing update", "name", name) res, err := u.performUpgrade(ctx, currentRelease, upgradedRelease, serverSideApply) if err != nil { return res, err @@ -201,7 +201,7 @@ func (u *Upgrade) RunWithContext(ctx context.Context, name string, ch chart.Char // Do not update for dry runs if !isDryRun(u.DryRunStrategy) { - slog.Debug("updating status for upgraded release", "name", name) + u.cfg.Logger().Debug("updating status for upgraded release", "name", name) if err := u.cfg.Releases.Update(upgradedRelease); err != nil { return res, err } @@ -308,7 +308,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chartv2.Chart, vals map[str return nil, nil, false, err } - slog.Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod)) + u.cfg.Logger().Debug("determined release apply method", slog.Bool("server_side_apply", serverSideApply), slog.String("previous_release_apply_method", lastRelease.ApplyMethod)) // Store an upgraded release. upgradedRelease := &release.Release{ @@ -391,7 +391,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR }) if isDryRun(u.DryRunStrategy) { - slog.Debug("dry run for release", "name", upgradedRelease.Name) + u.cfg.Logger().Debug("dry run for release", "name", upgradedRelease.Name) if len(u.Description) > 0 { upgradedRelease.Info.Description = u.Description } else { @@ -400,7 +400,7 @@ func (u *Upgrade) performUpgrade(ctx context.Context, originalRelease, upgradedR return upgradedRelease, nil } - slog.Debug("creating upgraded release", "name", upgradedRelease.Name) + u.cfg.Logger().Debug("creating upgraded release", "name", upgradedRelease.Name) if err := u.cfg.Releases.Create(upgradedRelease); err != nil { return nil, err } @@ -457,7 +457,7 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele return } } else { - slog.Debug("upgrade hooks disabled", "name", upgradedRelease.Name) + u.cfg.Logger().Debug("upgrade hooks disabled", "name", upgradedRelease.Name) } upgradeClientSideFieldManager := isReleaseApplyMethodClientSideApply(originalRelease.ApplyMethod) && serverSideApply // Update client-side field manager if transitioning from client-side to server-side apply @@ -515,13 +515,13 @@ func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *rele func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, err error) (*release.Release, error) { msg := fmt.Sprintf("Upgrade %q failed: %s", rel.Name, err) - slog.Warn("upgrade failed", "name", rel.Name, slog.Any("error", err)) + u.cfg.Logger().Warn("upgrade failed", "name", rel.Name, slog.Any("error", err)) rel.Info.Status = rcommon.StatusFailed rel.Info.Description = msg u.cfg.recordRelease(rel) if u.CleanupOnFail && len(created) > 0 { - slog.Debug("cleanup on fail set", "cleaning_resources", len(created)) + u.cfg.Logger().Debug("cleanup on fail set", "cleaning_resources", len(created)) _, errs := u.cfg.KubeClient.Delete(created, metav1.DeletePropagationBackground) if errs != nil { return rel, fmt.Errorf( @@ -533,11 +533,11 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e ), ) } - slog.Debug("resource cleanup complete") + u.cfg.Logger().Debug("resource cleanup complete") } if u.RollbackOnFailure { - slog.Debug("Upgrade failed and rollback-on-failure is set, rolling back to previous successful release") + u.cfg.Logger().Debug("Upgrade failed and rollback-on-failure is set, rolling back to previous successful release") // As a protection, get the last successful release before rollback. // If there are no successful releases, bail out @@ -594,13 +594,13 @@ func (u *Upgrade) failRelease(rel *release.Release, created kube.ResourceList, e func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, newVals map[string]interface{}) (map[string]interface{}, error) { if u.ResetValues { // If ResetValues is set, we completely ignore current.Config. - slog.Debug("resetting values to the chart's original version") + u.cfg.Logger().Debug("resetting values to the chart's original version") return newVals, nil } // If the ReuseValues flag is set, we always copy the old values over the new config's values. if u.ReuseValues { - slog.Debug("reusing the old release's values") + u.cfg.Logger().Debug("reusing the old release's values") // We have to regenerate the old coalesced values: oldVals, err := util.CoalesceValues(current.Chart, current.Config) @@ -617,7 +617,7 @@ func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, ne // If the ResetThenReuseValues flag is set, we use the new chart's values, but we copy the old config's values over the new config's values. if u.ResetThenReuseValues { - slog.Debug("merging values from old release to new values") + u.cfg.Logger().Debug("merging values from old release to new values") newVals = util.CoalesceTables(newVals, current.Config) @@ -625,7 +625,7 @@ func (u *Upgrade) reuseValues(chart *chartv2.Chart, current *release.Release, ne } if len(newVals) == 0 && len(current.Config) > 0 { - slog.Debug("copying values from old release", "name", current.Name, "version", current.Version) + u.cfg.Logger().Debug("copying values from old release", "name", current.Name, "version", current.Version) newVals = current.Config } return newVals, nil diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 48dbd760d..c23362628 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -103,7 +103,7 @@ By default, the default directories depend on the Operating System. The defaults var settings = cli.New() func NewRootCmd(out io.Writer, args []string, logSetup func(bool)) (*cobra.Command, error) { - actionConfig := new(action.Configuration) + actionConfig := action.NewConfiguration() cmd, err := newRootCmdWithConfig(actionConfig, out, args, logSetup) if err != nil { return nil, err diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 30c014ad5..b290cf721 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -30,6 +30,7 @@ import ( "reflect" "strings" "sync" + "sync/atomic" jsonpatch "github.com/evanphx/json-patch/v5" v1 "k8s.io/api/core/v1" @@ -81,6 +82,8 @@ type Client struct { Factory Factory // Namespace allows to bypass the kubeconfig file for the choice of the namespace Namespace string + // logger is an slog.Logger pointer to use with the kube client + logger atomic.Pointer[slog.Logger] Waiter kubeClient kubernetes.Interface @@ -177,6 +180,7 @@ func New(getter genericclioptions.RESTClientGetter) *Client { c := &Client{ Factory: factory, } + c.SetLogger(slog.Default()) return c } @@ -258,7 +262,7 @@ func ClientCreateOptionFieldValidationDirective(fieldValidationDirective FieldVa // Create creates Kubernetes resources specified in the resource list. func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) (*Result, error) { - slog.Debug("creating resource(s)", "resources", len(resources)) + c.Logger().Debug("creating resource(s)", "resources", len(resources)) createOptions := clientCreateOptions{ serverSideApply: true, // Default to server-side apply @@ -275,11 +279,11 @@ func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) ( makeCreateApplyFunc := func() func(target *resource.Info) error { if createOptions.serverSideApply { - slog.Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective))) + c.Logger().Debug("using server-side apply for resource creation", slog.Bool("forceConflicts", createOptions.forceConflicts), slog.Bool("dryRun", createOptions.dryRun), slog.String("fieldValidationDirective", string(createOptions.fieldValidationDirective))) return func(target *resource.Info) error { err := patchResourceServerSide(target, createOptions.dryRun, createOptions.forceConflicts, createOptions.fieldValidationDirective) - logger := slog.With( + logger := c.Logger().With( slog.String("namespace", target.Namespace), slog.String("name", target.Name), slog.String("gvk", target.Mapping.GroupVersionKind.String())) @@ -294,7 +298,7 @@ func (c *Client) Create(resources ResourceList, options ...ClientCreateOption) ( } } - slog.Debug("using client-side apply for resource creation") + c.Logger().Debug("using client-side apply for resource creation") return createResource } @@ -349,7 +353,7 @@ func (c *Client) Get(resources ResourceList, related bool) (map[string][]runtime objs, err = c.getSelectRelationPod(info, objs, isTable, &podSelectors) if err != nil { - slog.Warn("get the relation pod is failed", slog.Any("error", err)) + c.Logger().Warn("get the relation pod is failed", slog.Any("error", err)) } } } @@ -367,7 +371,7 @@ func (c *Client) getSelectRelationPod(info *resource.Info, objs map[string][]run if info == nil { return objs, nil } - slog.Debug("get relation pod of object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) + c.Logger().Debug("get relation pod of object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) selector, ok, _ := getSelectorFromObject(info.Object) if !ok { return objs, nil @@ -504,7 +508,7 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA updateErrors := []error{} res := &Result{} - slog.Debug("checking resources for changes", "resources", len(targets)) + c.Logger().Debug("checking resources for changes", "resources", len(targets)) err := targets.Visit(func(target *resource.Info, err error) error { if err != nil { return err @@ -525,7 +529,7 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA } kind := target.Mapping.GroupVersionKind.Kind - slog.Debug("created a new resource", "namespace", target.Namespace, "name", target.Name, "kind", kind) + c.Logger().Debug("created a new resource", "namespace", target.Namespace, "name", target.Name, "kind", kind) return nil } @@ -553,22 +557,22 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA } for _, info := range originals.Difference(targets) { - slog.Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) + c.Logger().Debug("deleting resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind) if err := info.Get(); err != nil { - slog.Debug("unable to get object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + c.Logger().Debug("unable to get object", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) continue } annotations, err := metadataAccessor.Annotations(info.Object) if err != nil { - slog.Debug("unable to get annotations", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + c.Logger().Debug("unable to get annotations", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) } if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy { - slog.Debug("skipping delete due to annotation", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, "annotation", ResourcePolicyAnno, "value", KeepPolicy) + c.Logger().Debug("skipping delete due to annotation", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, "annotation", ResourcePolicyAnno, "value", KeepPolicy) continue } if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil { - slog.Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + c.Logger().Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) continue } res.Deleted = append(res.Deleted, info) @@ -706,23 +710,23 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate makeUpdateApplyFunc := func() UpdateApplyFunc { if updateOptions.forceReplace { - slog.Debug( + c.Logger().Debug( "using resource replace update strategy", slog.String("fieldValidationDirective", string(updateOptions.fieldValidationDirective))) return func(original, target *resource.Info) error { if err := replaceResource(target, updateOptions.fieldValidationDirective); err != nil { - slog.Debug("error replacing the resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + c.Logger().Debug("error replacing the resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) return err } originalObject := original.Object kind := target.Mapping.GroupVersionKind.Kind - slog.Debug("replace succeeded", "name", original.Name, "initialKind", originalObject.GetObjectKind().GroupVersionKind().Kind, "kind", kind) + c.Logger().Debug("replace succeeded", "name", original.Name, "initialKind", originalObject.GetObjectKind().GroupVersionKind().Kind, "kind", kind) return nil } } else if updateOptions.serverSideApply { - slog.Debug( + c.Logger().Debug( "using server-side apply for resource update", slog.Bool("forceConflicts", updateOptions.forceConflicts), slog.Bool("dryRun", updateOptions.dryRun), @@ -730,7 +734,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate slog.Bool("upgradeClientSideFieldManager", updateOptions.upgradeClientSideFieldManager)) return func(original, target *resource.Info) error { - logger := slog.With( + logger := c.Logger().With( slog.String("namespace", target.Namespace), slog.String("name", target.Name), slog.String("gvk", target.Mapping.GroupVersionKind.String())) @@ -738,7 +742,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate if updateOptions.upgradeClientSideFieldManager { patched, err := upgradeClientSideFieldManager(original, updateOptions.dryRun, updateOptions.fieldValidationDirective) if err != nil { - slog.Debug("Error patching resource to replace CSA field management", slog.Any("error", err)) + c.Logger().Debug("Error patching resource to replace CSA field management", slog.Any("error", err)) return err } @@ -758,7 +762,7 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate } } - slog.Debug("using client-side apply for resource update", slog.Bool("threeWayMergeForUnstructured", updateOptions.threeWayMergeForUnstructured)) + c.Logger().Debug("using client-side apply for resource update", slog.Bool("threeWayMergeForUnstructured", updateOptions.threeWayMergeForUnstructured)) return func(original, target *resource.Info) error { return patchResourceClientSide(original.Object, target, updateOptions.threeWayMergeForUnstructured) } @@ -776,11 +780,11 @@ func (c *Client) Delete(resources ResourceList, policy metav1.DeletionPropagatio res := &Result{} mtx := sync.Mutex{} err := perform(resources, func(target *resource.Info) error { - slog.Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind) + c.Logger().Debug("starting delete resource", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind) err := deleteResource(target, policy) if err == nil || apierrors.IsNotFound(err) { if err != nil { - slog.Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) + c.Logger().Debug("ignoring delete failure", "namespace", target.Namespace, "name", target.Name, "kind", target.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) } mtx.Lock() defer mtx.Unlock() @@ -1187,3 +1191,20 @@ func (e *joinedErrors) Error() string { func (e *joinedErrors) Unwrap() []error { return e.errs } + +// logger returns the logger for the Client. If nil, returns slog.Default(). +func (c *Client) Logger() *slog.Logger { + if lg := c.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +func (c *Client) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + c.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + c.logger.Store(newLogger) +} diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index ada148158..e9093b8aa 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -22,6 +22,7 @@ import ( "log/slog" "strconv" "strings" + "sync/atomic" "time" v1 "k8s.io/api/core/v1" @@ -44,14 +45,18 @@ const ConfigMapsDriverName = "ConfigMap" // ConfigMapsInterface. type ConfigMaps struct { impl corev1.ConfigMapInterface + // logger is an slog.Logger pointer to use the driver + logger atomic.Pointer[slog.Logger] } // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of // the kubernetes ConfigMapsInterface. func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps { - return &ConfigMaps{ + c := &ConfigMaps{ impl: impl, } + c.SetLogger(slog.Default()) + return c } // Name returns the name of the driver. @@ -69,13 +74,13 @@ func (cfgmaps *ConfigMaps) Get(key string) (release.Releaser, error) { return nil, ErrReleaseNotFound } - slog.Debug("failed to get release", "key", key, slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to get release", "key", key, slog.Any("error", err)) return nil, err } // found the configmap, decode the base64 data string r, err := decodeRelease(obj.Data["release"]) if err != nil { - slog.Debug("failed to decode data", "key", key, slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to decode data", "key", key, slog.Any("error", err)) return nil, err } r.Labels = filterSystemLabels(obj.Labels) @@ -92,7 +97,7 @@ func (cfgmaps *ConfigMaps) List(filter func(release.Releaser) bool) ([]release.R list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { - slog.Debug("failed to list releases", slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to list releases", slog.Any("error", err)) return nil, err } @@ -103,7 +108,7 @@ func (cfgmaps *ConfigMaps) List(filter func(release.Releaser) bool) ([]release.R for _, item := range list.Items { rls, err := decodeRelease(item.Data["release"]) if err != nil { - slog.Debug("failed to decode release", "item", item, slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to decode release", "item", item, slog.Any("error", err)) continue } @@ -131,7 +136,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]release.Releaser, list, err := cfgmaps.impl.List(context.Background(), opts) if err != nil { - slog.Debug("failed to query with labels", slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to query with labels", slog.Any("error", err)) return nil, err } @@ -143,7 +148,7 @@ func (cfgmaps *ConfigMaps) Query(labels map[string]string) ([]release.Releaser, for _, item := range list.Items { rls, err := decodeRelease(item.Data["release"]) if err != nil { - slog.Debug("failed to decode release", slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to decode release", slog.Any("error", err)) continue } rls.Labels = item.Labels @@ -175,7 +180,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls release.Releaser) error { // create a new configmap to hold the release obj, err := newConfigMapsObject(key, rel, lbs) if err != nil { - slog.Debug("failed to encode release", "name", rac.Name(), slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to encode release", "name", rac.Name(), slog.Any("error", err)) return err } // push the configmap object out into the kubiverse @@ -184,7 +189,7 @@ func (cfgmaps *ConfigMaps) Create(key string, rls release.Releaser) error { return ErrReleaseExists } - slog.Debug("failed to create release", slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to create release", slog.Any("error", err)) return err } return nil @@ -208,13 +213,13 @@ func (cfgmaps *ConfigMaps) Update(key string, rel release.Releaser) error { // create a new configmap object to hold the release obj, err := newConfigMapsObject(key, rls, lbs) if err != nil { - slog.Debug("failed to encode release", "name", rls.Name, slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to encode release", "name", rls.Name, slog.Any("error", err)) return err } // push the configmap object out into the kubiverse _, err = cfgmaps.impl.Update(context.Background(), obj, metav1.UpdateOptions{}) if err != nil { - slog.Debug("failed to update release", slog.Any("error", err)) + cfgmaps.Logger().Debug("failed to update release", slog.Any("error", err)) return err } return nil @@ -276,3 +281,20 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigM Data: map[string]string{"release": s}, }, nil } + +// logger returns the logger for the ConfigMaps driver. If nil, returns slog.Default(). +func (cfgmaps *ConfigMaps) Logger() *slog.Logger { + if lg := cfgmaps.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +func (cfgmaps *ConfigMaps) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + cfgmaps.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + cfgmaps.logger.Store(newLogger) +} diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 352fe2c6a..7b25ef74c 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -17,9 +17,11 @@ limitations under the License. package driver import ( + "log/slog" "strconv" "strings" "sync" + "sync/atomic" "helm.sh/helm/v4/pkg/release" ) @@ -42,11 +44,15 @@ type Memory struct { namespace string // A map of namespaces to releases cache map[string]memReleases + // logger is an slog.Logger pointer to use the driver + logger atomic.Pointer[slog.Logger] } // NewMemory initializes a new memory driver. func NewMemory() *Memory { - return &Memory{cache: map[string]memReleases{}, namespace: "default"} + m := &Memory{cache: map[string]memReleases{}, namespace: "default"} + m.SetLogger(slog.Default()) + return m } // SetNamespace sets a specific namespace in which releases will be accessed. @@ -247,3 +253,19 @@ func (mem *Memory) rlock() func() { // ```defer unlock(mem.rlock())```, locks mem for reading at the // call point of defer and unlocks upon exiting the block. func unlock(fn func()) { fn() } + +func (mem *Memory) Logger() *slog.Logger { + if lg := mem.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +func (mem *Memory) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + mem.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + mem.logger.Store(newLogger) +} diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 1f5ce75ac..63b21cfdd 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -22,6 +22,7 @@ import ( "log/slog" "strconv" "strings" + "sync/atomic" "time" v1 "k8s.io/api/core/v1" @@ -44,14 +45,18 @@ const SecretsDriverName = "Secret" // SecretsInterface. type Secrets struct { impl corev1.SecretInterface + // logger is an slog.Logger pointer to use the driver + logger atomic.Pointer[slog.Logger] } // NewSecrets initializes a new Secrets wrapping an implementation of // the kubernetes SecretsInterface. func NewSecrets(impl corev1.SecretInterface) *Secrets { - return &Secrets{ + s := &Secrets{ impl: impl, } + s.SetLogger(slog.Default()) + return s } // Name returns the name of the driver. @@ -98,7 +103,7 @@ func (secrets *Secrets) List(filter func(release.Releaser) bool) ([]release.Rele for _, item := range list.Items { rls, err := decodeRelease(string(item.Data["release"])) if err != nil { - slog.Debug("list failed to decode release", "key", item.Name, slog.Any("error", err)) + secrets.Logger().Debug("list failed to decode release", "key", item.Name, slog.Any("error", err)) continue } @@ -137,7 +142,7 @@ func (secrets *Secrets) Query(labels map[string]string) ([]release.Releaser, err for _, item := range list.Items { rls, err := decodeRelease(string(item.Data["release"])) if err != nil { - slog.Debug("failed to decode release", "key", item.Name, slog.Any("error", err)) + secrets.Logger().Debug("failed to decode release", "key", item.Name, slog.Any("error", err)) continue } rls.Labels = item.Labels @@ -273,3 +278,20 @@ func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, er Data: map[string][]byte{"release": []byte(s)}, }, nil } + +// logger returns the logger for the Secrets driver. If nil, returns slog.Default(). +func (secrets *Secrets) Logger() *slog.Logger { + if lg := secrets.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +func (secrets *Secrets) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + secrets.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + secrets.logger.Store(newLogger) +} diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index 0020d2436..f02dda597 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -22,6 +22,7 @@ import ( "maps" "sort" "strconv" + "sync/atomic" "time" "github.com/jmoiron/sqlx" @@ -89,6 +90,8 @@ type SQL struct { db *sqlx.DB namespace string statementBuilder sq.StatementBuilderType + // logger is an slog.Logger pointer to use the driver + logger atomic.Pointer[slog.Logger] } // Name returns the name of the driver. @@ -109,13 +112,13 @@ func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool { records, err := migrate.GetMigrationRecords(s.db.DB, postgreSQLDialect) migrate.SetDisableCreateTable(false) if err != nil { - slog.Debug("failed to get migration records", slog.Any("error", err)) + s.Logger().Debug("failed to get migration records", slog.Any("error", err)) return false } for _, record := range records { if _, ok := migrationsIDs[record.Id]; ok { - slog.Debug("found previous migration", "id", record.Id, "appliedAt", record.AppliedAt) + s.Logger().Debug("found previous migration", "id", record.Id, "appliedAt", record.AppliedAt) delete(migrationsIDs, record.Id) } } @@ -123,7 +126,7 @@ func (s *SQL) checkAlreadyApplied(migrations []*migrate.Migration) bool { // check if all migrations applied if len(migrationsIDs) != 0 { for id := range migrationsIDs { - slog.Debug("find unapplied migration", "id", id) + s.Logger().Debug("find unapplied migration", "id", id) } return false } @@ -157,9 +160,9 @@ func (s *SQL) ensureDBSetup() error { CREATE INDEX ON %s (%s); CREATE INDEX ON %s (%s); CREATE INDEX ON %s (%s); - + GRANT ALL ON %s TO PUBLIC; - + ALTER TABLE %s ENABLE ROW LEVEL SECURITY; `, sqlReleaseTableName, @@ -209,7 +212,7 @@ func (s *SQL) ensureDBSetup() error { %s VARCHAR(%d) ); CREATE INDEX ON %s (%s, %s); - + GRANT ALL ON %s TO PUBLIC; ALTER TABLE %s ENABLE ROW LEVEL SECURITY; `, @@ -277,7 +280,7 @@ type SQLReleaseCustomLabelWrapper struct { } // NewSQL initializes a new sql driver. -func NewSQL(connectionString string, namespace string) (*SQL, error) { +func NewSQL(connectionString string, logger *slog.Logger, namespace string) (*SQL, error) { db, err := sqlx.Connect(postgreSQLDialect, connectionString) if err != nil { return nil, err @@ -293,6 +296,7 @@ func NewSQL(connectionString string, namespace string) (*SQL, error) { } driver.namespace = namespace + driver.SetLogger(logger) return driver, nil } @@ -309,24 +313,24 @@ func (s *SQL) Get(key string) (release.Releaser, error) { query, args, err := qb.ToSql() if err != nil { - slog.Debug("failed to build query", slog.Any("error", err)) + s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } // Get will return an error if the result is empty if err := s.db.Get(&record, query, args...); err != nil { - slog.Debug("got SQL error when getting release", "key", key, slog.Any("error", err)) + s.Logger().Debug("got SQL error when getting release", "key", key, slog.Any("error", err)) return nil, ErrReleaseNotFound } release, err := decodeRelease(record.Body) if err != nil { - slog.Debug("failed to decode data", "key", key, slog.Any("error", err)) + s.Logger().Debug("failed to decode data", "key", key, slog.Any("error", err)) return nil, err } if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil { - slog.Debug("failed to get release custom labels", "namespace", s.namespace, "key", key, slog.Any("error", err)) + s.Logger().Debug("failed to get release custom labels", "namespace", s.namespace, "key", key, slog.Any("error", err)) return nil, err } @@ -347,13 +351,13 @@ func (s *SQL) List(filter func(release.Releaser) bool) ([]release.Releaser, erro query, args, err := sb.ToSql() if err != nil { - slog.Debug("failed to build query", slog.Any("error", err)) + s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } var records = []SQLReleaseWrapper{} if err := s.db.Select(&records, query, args...); err != nil { - slog.Debug("failed to list", slog.Any("error", err)) + s.Logger().Debug("failed to list", slog.Any("error", err)) return nil, err } @@ -361,12 +365,12 @@ func (s *SQL) List(filter func(release.Releaser) bool) ([]release.Releaser, erro for _, record := range records { release, err := decodeRelease(record.Body) if err != nil { - slog.Debug("failed to decode release", "record", record, slog.Any("error", err)) + s.Logger().Debug("failed to decode release", "record", record, slog.Any("error", err)) continue } if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil { - slog.Debug("failed to get release custom labels", "namespace", record.Namespace, "key", record.Key, slog.Any("error", err)) + s.Logger().Debug("failed to get release custom labels", "namespace", record.Namespace, "key", record.Key, slog.Any("error", err)) return nil, err } maps.Copy(release.Labels, getReleaseSystemLabels(release)) @@ -394,7 +398,7 @@ func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) { if _, ok := labelMap[key]; ok { sb = sb.Where(sq.Eq{key: labels[key]}) } else { - slog.Debug("unknown label", "key", key) + s.Logger().Debug("unknown label", "key", key) return nil, fmt.Errorf("unknown label %s", key) } } @@ -407,13 +411,13 @@ func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) { // Build our query query, args, err := sb.ToSql() if err != nil { - slog.Debug("failed to build query", slog.Any("error", err)) + s.Logger().Debug("failed to build query", slog.Any("error", err)) return nil, err } var records = []SQLReleaseWrapper{} if err := s.db.Select(&records, query, args...); err != nil { - slog.Debug("failed to query with labels", slog.Any("error", err)) + s.Logger().Debug("failed to query with labels", slog.Any("error", err)) return nil, err } @@ -425,12 +429,12 @@ func (s *SQL) Query(labels map[string]string) ([]release.Releaser, error) { for _, record := range records { release, err := decodeRelease(record.Body) if err != nil { - slog.Debug("failed to decode release", "record", record, slog.Any("error", err)) + s.Logger().Debug("failed to decode release", "record", record, slog.Any("error", err)) continue } if release.Labels, err = s.getReleaseCustomLabels(record.Key, record.Namespace); err != nil { - slog.Debug("failed to get release custom labels", "namespace", record.Namespace, "key", record.Key, slog.Any("error", err)) + s.Logger().Debug("failed to get release custom labels", "namespace", record.Namespace, "key", record.Key, slog.Any("error", err)) return nil, err } @@ -459,13 +463,13 @@ func (s *SQL) Create(key string, rel release.Releaser) error { body, err := encodeRelease(rls) if err != nil { - slog.Debug("failed to encode release", slog.Any("error", err)) + s.Logger().Debug("failed to encode release", slog.Any("error", err)) return err } transaction, err := s.db.Beginx() if err != nil { - slog.Debug("failed to start SQL transaction", slog.Any("error", err)) + s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err)) return fmt.Errorf("error beginning transaction: %v", err) } @@ -494,7 +498,7 @@ func (s *SQL) Create(key string, rel release.Releaser) error { int(time.Now().Unix()), ).ToSql() if err != nil { - slog.Debug("failed to build insert query", slog.Any("error", err)) + s.Logger().Debug("failed to build insert query", slog.Any("error", err)) return err } @@ -508,17 +512,17 @@ func (s *SQL) Create(key string, rel release.Releaser) error { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if buildErr != nil { - slog.Debug("failed to build select query", "error", buildErr) + s.Logger().Debug("failed to build select query", "error", buildErr) return err } var record SQLReleaseWrapper if err := transaction.Get(&record, selectQuery, args...); err == nil { - slog.Debug("release already exists", "key", key) + s.Logger().Debug("release already exists", "key", key) return ErrReleaseExists } - slog.Debug("failed to store release in SQL database", "key", key, slog.Any("error", err)) + s.Logger().Debug("failed to store release in SQL database", "key", key, slog.Any("error", err)) return err } @@ -541,13 +545,13 @@ func (s *SQL) Create(key string, rel release.Releaser) error { if err != nil { defer transaction.Rollback() - slog.Debug("failed to build insert query", slog.Any("error", err)) + s.Logger().Debug("failed to build insert query", slog.Any("error", err)) return err } if _, err := transaction.Exec(insertLabelsQuery, args...); err != nil { defer transaction.Rollback() - slog.Debug("failed to write Labels", slog.Any("error", err)) + s.Logger().Debug("failed to write Labels", slog.Any("error", err)) return err } } @@ -570,7 +574,7 @@ func (s *SQL) Update(key string, rel release.Releaser) error { body, err := encodeRelease(rls) if err != nil { - slog.Debug("failed to encode release", slog.Any("error", err)) + s.Logger().Debug("failed to encode release", slog.Any("error", err)) return err } @@ -587,12 +591,12 @@ func (s *SQL) Update(key string, rel release.Releaser) error { ToSql() if err != nil { - slog.Debug("failed to build update query", slog.Any("error", err)) + s.Logger().Debug("failed to build update query", slog.Any("error", err)) return err } if _, err := s.db.Exec(query, args...); err != nil { - slog.Debug("failed to update release in SQL database", "key", key, slog.Any("error", err)) + s.Logger().Debug("failed to update release in SQL database", "key", key, slog.Any("error", err)) return err } @@ -603,7 +607,7 @@ func (s *SQL) Update(key string, rel release.Releaser) error { func (s *SQL) Delete(key string) (release.Releaser, error) { transaction, err := s.db.Beginx() if err != nil { - slog.Debug("failed to start SQL transaction", slog.Any("error", err)) + s.Logger().Debug("failed to start SQL transaction", slog.Any("error", err)) return nil, fmt.Errorf("error beginning transaction: %v", err) } @@ -614,20 +618,20 @@ func (s *SQL) Delete(key string) (release.Releaser, error) { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if err != nil { - slog.Debug("failed to build select query", slog.Any("error", err)) + s.Logger().Debug("failed to build select query", slog.Any("error", err)) return nil, err } var record SQLReleaseWrapper err = transaction.Get(&record, selectQuery, args...) if err != nil { - slog.Debug("release not found", "key", key, slog.Any("error", err)) + s.Logger().Debug("release not found", "key", key, slog.Any("error", err)) return nil, ErrReleaseNotFound } release, err := decodeRelease(record.Body) if err != nil { - slog.Debug("failed to decode release", "key", key, slog.Any("error", err)) + s.Logger().Debug("failed to decode release", "key", key, slog.Any("error", err)) transaction.Rollback() return nil, err } @@ -639,18 +643,18 @@ func (s *SQL) Delete(key string) (release.Releaser, error) { Where(sq.Eq{sqlReleaseTableNamespaceColumn: s.namespace}). ToSql() if err != nil { - slog.Debug("failed to build delete query", slog.Any("error", err)) + s.Logger().Debug("failed to build delete query", slog.Any("error", err)) return nil, err } _, err = transaction.Exec(deleteQuery, args...) if err != nil { - slog.Debug("failed perform delete query", slog.Any("error", err)) + s.Logger().Debug("failed perform delete query", slog.Any("error", err)) return release, err } if release.Labels, err = s.getReleaseCustomLabels(key, s.namespace); err != nil { - slog.Debug("failed to get release custom labels", "namespace", s.namespace, "key", key, slog.Any("error", err)) + s.Logger().Debug("failed to get release custom labels", "namespace", s.namespace, "key", key, slog.Any("error", err)) return nil, err } @@ -661,7 +665,7 @@ func (s *SQL) Delete(key string) (release.Releaser, error) { ToSql() if err != nil { - slog.Debug("failed to build delete Labels query", slog.Any("error", err)) + s.Logger().Debug("failed to build delete Labels query", slog.Any("error", err)) return nil, err } _, err = transaction.Exec(deleteCustomLabelsQuery, args...) @@ -702,3 +706,20 @@ func getReleaseSystemLabels(rls *rspb.Release) map[string]string { "version": strconv.Itoa(rls.Version), } } + +// logger returns the logger for the SQL driver. If nil, returns slog.Default(). +func (s *SQL) Logger() *slog.Logger { + if lg := s.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +func (s *SQL) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + s.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + s.logger.Store(newLogger) +} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 4603a1de6..86c5428df 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -21,6 +21,7 @@ import ( "fmt" "log/slog" "strings" + "sync/atomic" "helm.sh/helm/v4/pkg/release" "helm.sh/helm/v4/pkg/release/common" @@ -44,13 +45,16 @@ type Storage struct { // be retained, including the most recent release. Values of 0 or less are // ignored (meaning no limits are imposed). MaxHistory int + + // logger is an slog.Logger pointer to use the storage engine + logger atomic.Pointer[slog.Logger] } // Get retrieves the release from storage. An error is returned // if the storage driver failed to fetch the release, or the // release identified by the key, version pair does not exist. func (s *Storage) Get(name string, version int) (release.Releaser, error) { - slog.Debug("getting release", "key", makeKey(name, version)) + s.Logger().Debug("getting release", "key", makeKey(name, version)) return s.Driver.Get(makeKey(name, version)) } @@ -62,7 +66,7 @@ func (s *Storage) Create(rls release.Releaser) error { if err != nil { return err } - slog.Debug("creating release", "key", makeKey(rac.Name(), rac.Version())) + s.Logger().Debug("creating release", "key", makeKey(rac.Name(), rac.Version())) if s.MaxHistory > 0 { // Want to make space for one more release. if err := s.removeLeastRecent(rac.Name(), s.MaxHistory-1); err != nil && @@ -81,7 +85,7 @@ func (s *Storage) Update(rls release.Releaser) error { if err != nil { return err } - slog.Debug("updating release", "key", makeKey(rac.Name(), rac.Version())) + s.Logger().Debug("updating release", "key", makeKey(rac.Name(), rac.Version())) return s.Driver.Update(makeKey(rac.Name(), rac.Version()), rls) } @@ -89,14 +93,14 @@ func (s *Storage) Update(rls release.Releaser) error { // the storage backend fails to delete the release or if the release // does not exist. func (s *Storage) Delete(name string, version int) (release.Releaser, error) { - slog.Debug("deleting release", "key", makeKey(name, version)) + s.Logger().Debug("deleting release", "key", makeKey(name, version)) return s.Driver.Delete(makeKey(name, version)) } // ListReleases returns all releases from storage. An error is returned if the // storage backend fails to retrieve the releases. func (s *Storage) ListReleases() ([]release.Releaser, error) { - slog.Debug("listing all releases in storage") + s.Logger().Debug("listing all releases in storage") return s.List(func(_ release.Releaser) bool { return true }) } @@ -118,13 +122,13 @@ func releaserToV1Release(rel release.Releaser) (*rspb.Release, error) { // ListUninstalled returns all releases with Status == UNINSTALLED. An error is returned // if the storage backend fails to retrieve the releases. func (s *Storage) ListUninstalled() ([]release.Releaser, error) { - slog.Debug("listing uninstalled releases in storage") + s.Logger().Debug("listing uninstalled releases in storage") return s.List(func(rls release.Releaser) bool { rel, err := releaserToV1Release(rls) if err != nil { // This will only happen if calling code does not pass the proper types. This is // a problem with the application and not user data. - slog.Error("unable to convert release to typed release", slog.Any("error", err)) + s.Logger().Error("unable to convert release to typed release", slog.Any("error", err)) panic(fmt.Sprintf("unable to convert release to typed release: %s", err)) } return relutil.StatusFilter(common.StatusUninstalled).Check(rel) @@ -134,13 +138,13 @@ func (s *Storage) ListUninstalled() ([]release.Releaser, error) { // ListDeployed returns all releases with Status == DEPLOYED. An error is returned // if the storage backend fails to retrieve the releases. func (s *Storage) ListDeployed() ([]release.Releaser, error) { - slog.Debug("listing all deployed releases in storage") + s.Logger().Debug("listing all deployed releases in storage") return s.List(func(rls release.Releaser) bool { rel, err := releaserToV1Release(rls) if err != nil { // This will only happen if calling code does not pass the proper types. This is // a problem with the application and not user data. - slog.Error("unable to convert release to typed release", slog.Any("error", err)) + s.Logger().Error("unable to convert release to typed release", slog.Any("error", err)) panic(fmt.Sprintf("unable to convert release to typed release: %s", err)) } return relutil.StatusFilter(common.StatusDeployed).Check(rel) @@ -187,7 +191,7 @@ func releaseListToV1List(ls []release.Releaser) ([]*rspb.Release, error) { // DeployedAll returns all deployed releases with the provided name, or // returns driver.NewErrNoDeployedReleases if not found. func (s *Storage) DeployedAll(name string) ([]release.Releaser, error) { - slog.Debug("getting deployed releases", "name", name) + s.Logger().Debug("getting deployed releases", "name", name) ls, err := s.Query(map[string]string{ "name": name, @@ -206,7 +210,7 @@ func (s *Storage) DeployedAll(name string) ([]release.Releaser, error) { // History returns the revision history for the release with the provided name, or // returns driver.ErrReleaseNotFound if no such release name exists. func (s *Storage) History(name string) ([]release.Releaser, error) { - slog.Debug("getting release history", "name", name) + s.Logger().Debug("getting release history", "name", name) return s.Query(map[string]string{"name": name, "owner": "helm"}) } @@ -274,7 +278,7 @@ func (s *Storage) removeLeastRecent(name string, maximum int) error { } } - slog.Debug("pruned records", "count", len(toDelete), "release", name, "errors", len(errs)) + s.Logger().Debug("pruned records", "count", len(toDelete), "release", name, "errors", len(errs)) switch c := len(errs); c { case 0: return nil @@ -289,7 +293,7 @@ func (s *Storage) deleteReleaseVersion(name string, version int) error { key := makeKey(name, version) _, err := s.Delete(name, version) if err != nil { - slog.Debug("error pruning release", "key", key, slog.Any("error", err)) + s.Logger().Debug("error pruning release", "key", key, slog.Any("error", err)) return err } return nil @@ -297,7 +301,7 @@ func (s *Storage) deleteReleaseVersion(name string, version int) error { // Last fetches the last revision of the named release. func (s *Storage) Last(name string) (release.Releaser, error) { - slog.Debug("getting last revision", "name", name) + s.Logger().Debug("getting last revision", "name", name) h, err := s.History(name) if err != nil { return nil, err @@ -331,7 +335,26 @@ func Init(d driver.Driver) *Storage { if d == nil { d = driver.NewMemory() } - return &Storage{ + s := &Storage{ Driver: d, } + s.SetLogger(slog.Default()) + return s +} + +// logger returns the logger for the Storage. If nil, returns slog.Default(). +func (s *Storage) Logger() *slog.Logger { + if lg := s.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just be defensive +} + +func (s *Storage) SetLogger(newLogger *slog.Logger) { + // Only set logger if it's currently nil + if newLogger == nil { + s.logger.Store(slog.Default()) // We never want to set the logger to nil + return + } + s.logger.Store(newLogger) } From 7a5816b106ae2aca4dc952ca580c09979177d65b Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Tue, 21 Oct 2025 23:53:09 +0100 Subject: [PATCH 103/118] Self review changes Signed-off-by: Evans Mungai --- pkg/action/action.go | 1 - pkg/kube/client.go | 2 +- pkg/storage/driver/cfgmaps.go | 2 +- pkg/storage/driver/memory.go | 2 +- pkg/storage/driver/secrets.go | 2 +- pkg/storage/driver/sql.go | 6 +++--- pkg/storage/storage.go | 2 +- 7 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 4641b7427..cde915989 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -530,7 +530,6 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp case "sql": d, err := driver.NewSQL( os.Getenv("HELM_DRIVER_SQL_CONNECTION_STRING"), - cfg.Logger(), namespace, ) if err != nil { diff --git a/pkg/kube/client.go b/pkg/kube/client.go index a8023516a..88456ca23 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1204,7 +1204,7 @@ func (c *Client) Logger() *slog.Logger { if lg := c.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just be defensive + return slog.Default() // We rarely get here, just being defensive } func (c *Client) SetLogger(newLogger *slog.Logger) { diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index e9093b8aa..73934a94b 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -287,7 +287,7 @@ func (cfgmaps *ConfigMaps) Logger() *slog.Logger { if lg := cfgmaps.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just be defensive + return slog.Default() // We rarely get here, just being defensive } func (cfgmaps *ConfigMaps) SetLogger(newLogger *slog.Logger) { diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index 7b25ef74c..ed9444260 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -258,7 +258,7 @@ func (mem *Memory) Logger() *slog.Logger { if lg := mem.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just be defensive + return slog.Default() // We rarely get here, just being defensive } func (mem *Memory) SetLogger(newLogger *slog.Logger) { diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 63b21cfdd..7cdddb585 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -284,7 +284,7 @@ func (secrets *Secrets) Logger() *slog.Logger { if lg := secrets.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just be defensive + return slog.Default() // We rarely get here, just being defensive } func (secrets *Secrets) SetLogger(newLogger *slog.Logger) { diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index f02dda597..8a0c7789b 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -280,7 +280,7 @@ type SQLReleaseCustomLabelWrapper struct { } // NewSQL initializes a new sql driver. -func NewSQL(connectionString string, logger *slog.Logger, namespace string) (*SQL, error) { +func NewSQL(connectionString string, namespace string) (*SQL, error) { db, err := sqlx.Connect(postgreSQLDialect, connectionString) if err != nil { return nil, err @@ -296,7 +296,7 @@ func NewSQL(connectionString string, logger *slog.Logger, namespace string) (*SQ } driver.namespace = namespace - driver.SetLogger(logger) + driver.SetLogger(slog.Default()) return driver, nil } @@ -712,7 +712,7 @@ func (s *SQL) Logger() *slog.Logger { if lg := s.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just be defensive + return slog.Default() // We rarely get here, just being defensive } func (s *SQL) SetLogger(newLogger *slog.Logger) { diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 86c5428df..290bca2ba 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -347,7 +347,7 @@ func (s *Storage) Logger() *slog.Logger { if lg := s.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just be defensive + return slog.Default() // We rarely get here, just being defensive } func (s *Storage) SetLogger(newLogger *slog.Logger) { From 50e43f401719c4c5bcd1ddaf953726a460725539 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 00:40:03 +0100 Subject: [PATCH 104/118] nil logger should be handled by discard handler Signed-off-by: Evans Mungai --- pkg/action/action.go | 3 +-- pkg/kube/client.go | 3 +-- pkg/storage/driver/cfgmaps.go | 3 +-- pkg/storage/driver/memory.go | 3 +-- pkg/storage/driver/secrets.go | 3 +-- pkg/storage/driver/sql.go | 3 +-- pkg/storage/storage.go | 3 +-- 7 files changed, 7 insertions(+), 14 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index cde915989..92ca778cb 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -564,9 +564,8 @@ func (cfg *Configuration) Logger() *slog.Logger { // SetLogger sets the logger for the Configuration. If nil, sets the default logger. func (cfg *Configuration) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - cfg.logger.Store(slog.Default()) // We never want to set the logger to nil + cfg.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } cfg.logger.Store(newLogger) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 88456ca23..cdd95d66f 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -1208,9 +1208,8 @@ func (c *Client) Logger() *slog.Logger { } func (c *Client) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - c.logger.Store(slog.Default()) // We never want to set the logger to nil + c.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } c.logger.Store(newLogger) diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 73934a94b..4d356a2c4 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -291,9 +291,8 @@ func (cfgmaps *ConfigMaps) Logger() *slog.Logger { } func (cfgmaps *ConfigMaps) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - cfgmaps.logger.Store(slog.Default()) // We never want to set the logger to nil + cfgmaps.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } cfgmaps.logger.Store(newLogger) diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index ed9444260..dc950df36 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -262,9 +262,8 @@ func (mem *Memory) Logger() *slog.Logger { } func (mem *Memory) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - mem.logger.Store(slog.Default()) // We never want to set the logger to nil + mem.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } mem.logger.Store(newLogger) diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 7cdddb585..d4a18e966 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -288,9 +288,8 @@ func (secrets *Secrets) Logger() *slog.Logger { } func (secrets *Secrets) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - secrets.logger.Store(slog.Default()) // We never want to set the logger to nil + secrets.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } secrets.logger.Store(newLogger) diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index 8a0c7789b..df7d18ae2 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -716,9 +716,8 @@ func (s *SQL) Logger() *slog.Logger { } func (s *SQL) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - s.logger.Store(slog.Default()) // We never want to set the logger to nil + s.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } s.logger.Store(newLogger) diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 290bca2ba..dd8218897 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -351,9 +351,8 @@ func (s *Storage) Logger() *slog.Logger { } func (s *Storage) SetLogger(newLogger *slog.Logger) { - // Only set logger if it's currently nil if newLogger == nil { - s.logger.Store(slog.Default()) // We never want to set the logger to nil + s.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } s.logger.Store(newLogger) From 9c32e34d60f20498e513935076e38af3b53169d9 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 13:29:37 +0100 Subject: [PATCH 105/118] Add logger to sql driver and ensure storage has logger Signed-off-by: Evans Mungai --- internal/logging/logging.go | 8 ++++++++ pkg/action/action.go | 2 +- pkg/storage/storage.go | 10 +++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 2e8208d08..8fd3082b5 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -85,3 +85,11 @@ func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger { return slog.New(dynamicHandler) } + +// LoggerSetterGetter is an interface that can set and get a logger +type LoggerSetterGetter interface { + // SetLogger sets the logger for the object + SetLogger(logger *slog.Logger) + // Logger returns the logger for the object + Logger() *slog.Logger +} diff --git a/pkg/action/action.go b/pkg/action/action.go index 92ca778cb..75575b3f3 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -535,6 +535,7 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp if err != nil { return fmt.Errorf("unable to instantiate SQL driver: %w", err) } + d.SetLogger(cfg.Logger()) store = storage.Init(d) default: return fmt.Errorf("unknown driver %q", helmDriver) @@ -543,7 +544,6 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp cfg.RESTClientGetter = getter cfg.KubeClient = kc cfg.Releases = store - cfg.Releases.SetLogger(cfg.Logger()) cfg.HookOutputFunc = func(_, _, _ string) io.Writer { return io.Discard } return nil diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index dd8218897..e55c628cf 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -23,6 +23,7 @@ import ( "strings" "sync/atomic" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/release" "helm.sh/helm/v4/pkg/release/common" rspb "helm.sh/helm/v4/pkg/release/v1" @@ -338,7 +339,14 @@ func Init(d driver.Driver) *Storage { s := &Storage{ Driver: d, } - s.SetLogger(slog.Default()) + + // Get logger from driver if it implements the LoggerSetterGetter interface + if ls, ok := d.(logging.LoggerSetterGetter); ok { + ls.SetLogger(s.Logger()) + } else { + // If the driver does not implement the LoggerSetterGetter interface, set the default logger + s.SetLogger(slog.Default()) + } return s } From 5ab4ca5490d34307b0d322b6ba76a645d274a78e Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 13:55:12 +0100 Subject: [PATCH 106/118] Embed logging functionality to DRY code Signed-off-by: Evans Mungai --- internal/logging/logging.go | 26 ++++++++++++++++++++++++++ pkg/action/action.go | 25 ++++--------------------- pkg/kube/client.go | 24 +++++------------------- pkg/storage/driver/cfgmaps.go | 23 ++++------------------- pkg/storage/driver/memory.go | 21 +++------------------ pkg/storage/driver/secrets.go | 22 +++------------------- pkg/storage/driver/sql.go | 22 +++------------------- pkg/storage/storage.go | 21 ++------------------- 8 files changed, 50 insertions(+), 134 deletions(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 8fd3082b5..0e95a9875 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -20,6 +20,7 @@ import ( "context" "log/slog" "os" + "sync/atomic" ) // DebugEnabledFunc is a function type that determines if debug logging is enabled @@ -93,3 +94,28 @@ type LoggerSetterGetter interface { // Logger returns the logger for the object Logger() *slog.Logger } + +type LogHolder struct { + // logger is an slog.Logger pointer to use the driver + logger atomic.Pointer[slog.Logger] +} + +// Logger returns the logger for the LogHolder. If nil, returns slog.Default(). +func (l *LogHolder) Logger() *slog.Logger { + if lg := l.logger.Load(); lg != nil { + return lg + } + return slog.Default() // We rarely get here, just being defensive +} + +// SetLogger sets the logger for the LogHolder. If nil, sets the default logger. +func (l *LogHolder) SetLogger(newLogger *slog.Logger) { + if newLogger == nil { + l.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs + return + } + l.logger.Store(newLogger) +} + +// Ensure LogHolder implements LoggerSetterGetter +var _ LoggerSetterGetter = &LogHolder{} diff --git a/pkg/action/action.go b/pkg/action/action.go index 75575b3f3..446dcd79d 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -29,7 +29,6 @@ import ( "slices" "strings" "sync" - "sync/atomic" "text/template" "time" @@ -41,6 +40,7 @@ import ( "sigs.k8s.io/kustomize/kyaml/kio" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" chartutil "helm.sh/helm/v4/pkg/chart/v2/util" @@ -110,11 +110,11 @@ type Configuration struct { // HookOutputFunc called with container name and returns and expects writer that will receive the log output. HookOutputFunc func(namespace, pod, container string) io.Writer - // logger is an slog.Logger pointer to use with the Configuration instance - logger atomic.Pointer[slog.Logger] - // Mutex is an exclusive lock for concurrent access to the action mutex sync.Mutex + + // Embed a LogHolder to provide logger functionality + logging.LogHolder } func NewConfiguration() *Configuration { @@ -554,23 +554,6 @@ func (cfg *Configuration) SetHookOutputFunc(hookOutputFunc func(_, _, _ string) cfg.HookOutputFunc = hookOutputFunc } -// Logger returns the logger for the Configuration. If nil, returns slog.Default(). -func (cfg *Configuration) Logger() *slog.Logger { - if lg := cfg.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just be defensive -} - -// SetLogger sets the logger for the Configuration. If nil, sets the default logger. -func (cfg *Configuration) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - cfg.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - cfg.logger.Store(newLogger) -} - func determineReleaseSSApplyMethod(serverSideApply bool) release.ApplyMethod { if serverSideApply { return release.ApplyMethodServerSideApply diff --git a/pkg/kube/client.go b/pkg/kube/client.go index cdd95d66f..9fe26bcc9 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -30,7 +30,6 @@ import ( "reflect" "strings" "sync" - "sync/atomic" jsonpatch "github.com/evanphx/json-patch/v5" v1 "k8s.io/api/core/v1" @@ -40,6 +39,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + "helm.sh/helm/v4/internal/logging" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -82,11 +83,12 @@ type Client struct { Factory Factory // Namespace allows to bypass the kubeconfig file for the choice of the namespace Namespace string - // logger is an slog.Logger pointer to use with the kube client - logger atomic.Pointer[slog.Logger] Waiter kubeClient kubernetes.Interface + + // Embed a LogHolder to provide logger functionality + logging.LogHolder } var _ Interface = (*Client)(nil) @@ -1198,19 +1200,3 @@ func (e *joinedErrors) Error() string { func (e *joinedErrors) Unwrap() []error { return e.errs } - -// logger returns the logger for the Client. If nil, returns slog.Default(). -func (c *Client) Logger() *slog.Logger { - if lg := c.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just being defensive -} - -func (c *Client) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - c.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - c.logger.Store(newLogger) -} diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 4d356a2c4..81e22ef40 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -22,7 +22,6 @@ import ( "log/slog" "strconv" "strings" - "sync/atomic" "time" v1 "k8s.io/api/core/v1" @@ -32,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -45,8 +45,9 @@ const ConfigMapsDriverName = "ConfigMap" // ConfigMapsInterface. type ConfigMaps struct { impl corev1.ConfigMapInterface - // logger is an slog.Logger pointer to use the driver - logger atomic.Pointer[slog.Logger] + + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // NewConfigMaps initializes a new ConfigMaps wrapping an implementation of @@ -281,19 +282,3 @@ func newConfigMapsObject(key string, rls *rspb.Release, lbs labels) (*v1.ConfigM Data: map[string]string{"release": s}, }, nil } - -// logger returns the logger for the ConfigMaps driver. If nil, returns slog.Default(). -func (cfgmaps *ConfigMaps) Logger() *slog.Logger { - if lg := cfgmaps.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just being defensive -} - -func (cfgmaps *ConfigMaps) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - cfgmaps.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - cfgmaps.logger.Store(newLogger) -} diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index dc950df36..ec7470aac 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -21,8 +21,8 @@ import ( "strconv" "strings" "sync" - "sync/atomic" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/release" ) @@ -44,8 +44,8 @@ type Memory struct { namespace string // A map of namespaces to releases cache map[string]memReleases - // logger is an slog.Logger pointer to use the driver - logger atomic.Pointer[slog.Logger] + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // NewMemory initializes a new memory driver. @@ -253,18 +253,3 @@ func (mem *Memory) rlock() func() { // ```defer unlock(mem.rlock())```, locks mem for reading at the // call point of defer and unlocks upon exiting the block. func unlock(fn func()) { fn() } - -func (mem *Memory) Logger() *slog.Logger { - if lg := mem.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just being defensive -} - -func (mem *Memory) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - mem.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - mem.logger.Store(newLogger) -} diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index d4a18e966..2c29c436b 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -22,7 +22,6 @@ import ( "log/slog" "strconv" "strings" - "sync/atomic" "time" v1 "k8s.io/api/core/v1" @@ -32,6 +31,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -45,8 +45,8 @@ const SecretsDriverName = "Secret" // SecretsInterface. type Secrets struct { impl corev1.SecretInterface - // logger is an slog.Logger pointer to use the driver - logger atomic.Pointer[slog.Logger] + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // NewSecrets initializes a new Secrets wrapping an implementation of @@ -278,19 +278,3 @@ func newSecretsObject(key string, rls *rspb.Release, lbs labels) (*v1.Secret, er Data: map[string][]byte{"release": []byte(s)}, }, nil } - -// logger returns the logger for the Secrets driver. If nil, returns slog.Default(). -func (secrets *Secrets) Logger() *slog.Logger { - if lg := secrets.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just being defensive -} - -func (secrets *Secrets) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - secrets.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - secrets.logger.Store(newLogger) -} diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index df7d18ae2..c1e73e2dd 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -22,7 +22,6 @@ import ( "maps" "sort" "strconv" - "sync/atomic" "time" "github.com/jmoiron/sqlx" @@ -33,6 +32,7 @@ import ( // Import pq for postgres dialect _ "github.com/lib/pq" + "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/release" rspb "helm.sh/helm/v4/pkg/release/v1" ) @@ -90,8 +90,8 @@ type SQL struct { db *sqlx.DB namespace string statementBuilder sq.StatementBuilderType - // logger is an slog.Logger pointer to use the driver - logger atomic.Pointer[slog.Logger] + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // Name returns the name of the driver. @@ -706,19 +706,3 @@ func getReleaseSystemLabels(rls *rspb.Release) map[string]string { "version": strconv.Itoa(rls.Version), } } - -// logger returns the logger for the SQL driver. If nil, returns slog.Default(). -func (s *SQL) Logger() *slog.Logger { - if lg := s.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just being defensive -} - -func (s *SQL) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - s.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - s.logger.Store(newLogger) -} diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index e55c628cf..d8b141dc9 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -21,7 +21,6 @@ import ( "fmt" "log/slog" "strings" - "sync/atomic" "helm.sh/helm/v4/internal/logging" "helm.sh/helm/v4/pkg/release" @@ -47,8 +46,8 @@ type Storage struct { // ignored (meaning no limits are imposed). MaxHistory int - // logger is an slog.Logger pointer to use the storage engine - logger atomic.Pointer[slog.Logger] + // Embed a LogHolder to provide logger functionality + logging.LogHolder } // Get retrieves the release from storage. An error is returned @@ -349,19 +348,3 @@ func Init(d driver.Driver) *Storage { } return s } - -// logger returns the logger for the Storage. If nil, returns slog.Default(). -func (s *Storage) Logger() *slog.Logger { - if lg := s.logger.Load(); lg != nil { - return lg - } - return slog.Default() // We rarely get here, just being defensive -} - -func (s *Storage) SetLogger(newLogger *slog.Logger) { - if newLogger == nil { - s.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs - return - } - s.logger.Store(newLogger) -} From b6eca1c0f1694e575c3795f5f9d710cc0f386f60 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 15:26:48 +0100 Subject: [PATCH 107/118] Refactor logging functionality to use slog.Handler Signed-off-by: Evans Mungai --- internal/logging/logging.go | 12 ++++++------ pkg/action/action.go | 12 ++++++------ pkg/kube/client.go | 2 +- pkg/storage/driver/cfgmaps.go | 2 +- pkg/storage/driver/memory.go | 2 +- pkg/storage/driver/secrets.go | 2 +- pkg/storage/driver/sql.go | 2 +- pkg/storage/storage.go | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 0e95a9875..1c7f54e89 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -89,14 +89,14 @@ func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger { // LoggerSetterGetter is an interface that can set and get a logger type LoggerSetterGetter interface { - // SetLogger sets the logger for the object - SetLogger(logger *slog.Logger) - // Logger returns the logger for the object + // SetLogger sets a new slog.Handler + SetLogger(newLogger slog.Handler) + // Logger returns the slog.Logger created from the slog.Handler Logger() *slog.Logger } type LogHolder struct { - // logger is an slog.Logger pointer to use the driver + // logger is an atomic.Pointer[slog.Logger] to store the slog.Logger logger atomic.Pointer[slog.Logger] } @@ -109,12 +109,12 @@ func (l *LogHolder) Logger() *slog.Logger { } // SetLogger sets the logger for the LogHolder. If nil, sets the default logger. -func (l *LogHolder) SetLogger(newLogger *slog.Logger) { +func (l *LogHolder) SetLogger(newLogger slog.Handler) { if newLogger == nil { l.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } - l.logger.Store(newLogger) + l.logger.Store(slog.New(newLogger)) } // Ensure LogHolder implements LoggerSetterGetter diff --git a/pkg/action/action.go b/pkg/action/action.go index 446dcd79d..04afdb133 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -119,7 +119,7 @@ type Configuration struct { func NewConfiguration() *Configuration { c := &Configuration{} - c.SetLogger(slog.Default()) + c.SetLogger(slog.Default().Handler()) return c } @@ -494,7 +494,7 @@ func (cfg *Configuration) recordRelease(r *release.Release) { // Init initializes the action configuration func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace, helmDriver string) error { kc := kube.New(getter) - kc.SetLogger(cfg.Logger()) + kc.SetLogger(cfg.Logger().Handler()) lazyClient := &lazyClient{ namespace: namespace, @@ -505,11 +505,11 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp switch helmDriver { case "secret", "secrets", "": d := driver.NewSecrets(newSecretClient(lazyClient)) - d.SetLogger(cfg.Logger()) + d.SetLogger(cfg.Logger().Handler()) store = storage.Init(d) case "configmap", "configmaps": d := driver.NewConfigMaps(newConfigMapClient(lazyClient)) - d.SetLogger(cfg.Logger()) + d.SetLogger(cfg.Logger().Handler()) store = storage.Init(d) case "memory": var d *driver.Memory @@ -524,7 +524,7 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp if d == nil { d = driver.NewMemory() } - d.SetLogger(cfg.Logger()) + d.SetLogger(cfg.Logger().Handler()) d.SetNamespace(namespace) store = storage.Init(d) case "sql": @@ -535,7 +535,7 @@ func (cfg *Configuration) Init(getter genericclioptions.RESTClientGetter, namesp if err != nil { return fmt.Errorf("unable to instantiate SQL driver: %w", err) } - d.SetLogger(cfg.Logger()) + d.SetLogger(cfg.Logger().Handler()) store = storage.Init(d) default: return fmt.Errorf("unknown driver %q", helmDriver) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 9fe26bcc9..4442a0d91 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -182,7 +182,7 @@ func New(getter genericclioptions.RESTClientGetter) *Client { c := &Client{ Factory: factory, } - c.SetLogger(slog.Default()) + c.SetLogger(slog.Default().Handler()) return c } diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index 81e22ef40..5af432d8a 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -56,7 +56,7 @@ func NewConfigMaps(impl corev1.ConfigMapInterface) *ConfigMaps { c := &ConfigMaps{ impl: impl, } - c.SetLogger(slog.Default()) + c.SetLogger(slog.Default().Handler()) return c } diff --git a/pkg/storage/driver/memory.go b/pkg/storage/driver/memory.go index ec7470aac..7ea4a014a 100644 --- a/pkg/storage/driver/memory.go +++ b/pkg/storage/driver/memory.go @@ -51,7 +51,7 @@ type Memory struct { // NewMemory initializes a new memory driver. func NewMemory() *Memory { m := &Memory{cache: map[string]memReleases{}, namespace: "default"} - m.SetLogger(slog.Default()) + m.SetLogger(slog.Default().Handler()) return m } diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index 2c29c436b..85f3497e7 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -55,7 +55,7 @@ func NewSecrets(impl corev1.SecretInterface) *Secrets { s := &Secrets{ impl: impl, } - s.SetLogger(slog.Default()) + s.SetLogger(slog.Default().Handler()) return s } diff --git a/pkg/storage/driver/sql.go b/pkg/storage/driver/sql.go index c1e73e2dd..b6ea3916d 100644 --- a/pkg/storage/driver/sql.go +++ b/pkg/storage/driver/sql.go @@ -296,7 +296,7 @@ func NewSQL(connectionString string, namespace string) (*SQL, error) { } driver.namespace = namespace - driver.SetLogger(slog.Default()) + driver.SetLogger(slog.Default().Handler()) return driver, nil } diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index d8b141dc9..07dc12c7b 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -341,10 +341,10 @@ func Init(d driver.Driver) *Storage { // Get logger from driver if it implements the LoggerSetterGetter interface if ls, ok := d.(logging.LoggerSetterGetter); ok { - ls.SetLogger(s.Logger()) + ls.SetLogger(s.Logger().Handler()) } else { // If the driver does not implement the LoggerSetterGetter interface, set the default logger - s.SetLogger(slog.Default()) + s.SetLogger(slog.Default().Handler()) } return s } From d67b17b6ec966c22afbfc2a6ec5b6f0f32c58813 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 15:30:02 +0100 Subject: [PATCH 108/118] Minor name change Signed-off-by: Evans Mungai --- internal/logging/logging.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 1c7f54e89..2fca0457f 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -90,7 +90,7 @@ func NewLogger(debugEnabled DebugEnabledFunc) *slog.Logger { // LoggerSetterGetter is an interface that can set and get a logger type LoggerSetterGetter interface { // SetLogger sets a new slog.Handler - SetLogger(newLogger slog.Handler) + SetLogger(newHandler slog.Handler) // Logger returns the slog.Logger created from the slog.Handler Logger() *slog.Logger } @@ -109,12 +109,12 @@ func (l *LogHolder) Logger() *slog.Logger { } // SetLogger sets the logger for the LogHolder. If nil, sets the default logger. -func (l *LogHolder) SetLogger(newLogger slog.Handler) { - if newLogger == nil { +func (l *LogHolder) SetLogger(newHandler slog.Handler) { + if newHandler == nil { l.logger.Store(slog.New(slog.DiscardHandler)) // Assume nil as discarding logs return } - l.logger.Store(slog.New(newLogger)) + l.logger.Store(slog.New(newHandler)) } // Ensure LogHolder implements LoggerSetterGetter From 99fe9b3b9b2bb8c924fe1a4aaedfbe7d4077841a Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 16:20:16 +0100 Subject: [PATCH 109/118] Update logging to use a param for the logger Signed-off-by: Evans Mungai --- internal/logging/logging.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 2fca0457f..90cccc0cd 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -97,6 +97,7 @@ type LoggerSetterGetter interface { type LogHolder struct { // logger is an atomic.Pointer[slog.Logger] to store the slog.Logger + // We use atomic.Pointer for thread safety logger atomic.Pointer[slog.Logger] } From e63b4d92cbc5d9d2bca279555eaa86b2a2fe9cbf Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 19:08:41 +0100 Subject: [PATCH 110/118] Update internal/logging/logging.go Co-authored-by: George Jenkins Signed-off-by: Evans Mungai --- internal/logging/logging.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 90cccc0cd..b8faf859e 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -106,7 +106,7 @@ func (l *LogHolder) Logger() *slog.Logger { if lg := l.logger.Load(); lg != nil { return lg } - return slog.Default() // We rarely get here, just being defensive + return slog.New(slog.DiscardHandler) // Should never be reached } // SetLogger sets the logger for the LogHolder. If nil, sets the default logger. From aed687eaa161da939b9052bf1b13371b4f1cdc32 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Wed, 22 Oct 2025 19:18:32 +0100 Subject: [PATCH 111/118] Add config options to NewConfiguration() Signed-off-by: Evans Mungai --- pkg/action/action.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 04afdb133..1d16ba7b8 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -117,9 +117,26 @@ type Configuration struct { logging.LogHolder } -func NewConfiguration() *Configuration { +type ConfigurationOption func(c *Configuration) + +// Override the default logging handler +// If unspecified, the default logger will be used +func ConfigurationSetLogger(h slog.Handler) ConfigurationOption { + return func(c *Configuration) { + c.SetLogger(h) + } +} + +func NewConfiguration(options ...ConfigurationOption) *Configuration { c := &Configuration{} - c.SetLogger(slog.Default().Handler()) + for _, o := range options { + o(c) + } + + if c.Logger() == nil { + c.SetLogger(slog.Default().Handler()) + } + return c } From a112bf5aa6a86433f1b1e0702ded21d3a65790ff Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 23 Oct 2025 09:53:25 +0100 Subject: [PATCH 112/118] Remove non-reachable code Signed-off-by: Evans Mungai --- pkg/action/action.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/action/action.go b/pkg/action/action.go index 1d16ba7b8..1104386d2 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -133,10 +133,6 @@ func NewConfiguration(options ...ConfigurationOption) *Configuration { o(c) } - if c.Logger() == nil { - c.SetLogger(slog.Default().Handler()) - } - return c } From 28bc76c3643a7c33dd0838a37d43945061735ed9 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 23 Oct 2025 10:44:17 +0100 Subject: [PATCH 113/118] Add tests for logging LogHolder Signed-off-by: Evans Mungai --- internal/logging/logging_test.go | 115 +++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 internal/logging/logging_test.go diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go new file mode 100644 index 000000000..5d354e330 --- /dev/null +++ b/internal/logging/logging_test.go @@ -0,0 +1,115 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package logging + +import ( + "bytes" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLogHolder_Logger(t *testing.T) { + t.Run("should return new logger with a then set handler", func(t *testing.T) { + holder := &LogHolder{} + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + + holder.SetLogger(handler) + logger := holder.Logger() + + assert.NotNil(t, logger) + + // Test that the logger works + logger.Info("test message") + assert.Contains(t, buf.String(), "test message") + }) + + t.Run("should return discard - defaultlogger when no handler is set", func(t *testing.T) { + holder := &LogHolder{} + logger := holder.Logger() + + assert.Equal(t, slog.Default().Handler(), logger.Handler()) + }) +} + +func TestLogHolder_SetLogger(t *testing.T) { + t.Run("sets logger with valid handler", func(t *testing.T) { + holder := &LogHolder{} + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + + holder.SetLogger(handler) + logger := holder.Logger() + + assert.NotNil(t, logger) + + // Compare the handler directly + assert.Equal(t, handler, logger.Handler()) + }) + + t.Run("sets discard logger with nil handler", func(t *testing.T) { + holder := &LogHolder{} + + holder.SetLogger(nil) + logger := holder.Logger() + + assert.NotNil(t, logger) + + assert.Equal(t, slog.Handler(slog.DiscardHandler), logger.Handler()) + }) + + t.Run("can replace existing logger", func(t *testing.T) { + holder := &LogHolder{} + + // Set first logger + buf1 := &bytes.Buffer{} + handler1 := slog.NewTextHandler(buf1, &slog.HandlerOptions{Level: slog.LevelDebug}) + holder.SetLogger(handler1) + + logger1 := holder.Logger() + assert.Equal(t, handler1, logger1.Handler()) + + // Replace with second logger + buf2 := &bytes.Buffer{} + handler2 := slog.NewTextHandler(buf2, &slog.HandlerOptions{Level: slog.LevelDebug}) + holder.SetLogger(handler2) + + logger2 := holder.Logger() + assert.Equal(t, handler2, logger2.Handler()) + }) +} + +func TestLogHolder_InterfaceCompliance(t *testing.T) { + t.Run("implements LoggerSetterGetter interface", func(_ *testing.T) { + var _ LoggerSetterGetter = &LogHolder{} + }) + + t.Run("interface methods work correctly", func(t *testing.T) { + var holder LoggerSetterGetter = &LogHolder{} + + buf := &bytes.Buffer{} + handler := slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + + holder.SetLogger(handler) + logger := holder.Logger() + + assert.NotNil(t, logger) + assert.Equal(t, handler, logger.Handler()) + }) +} From a94878fb8946b04dd9dfaa0d906fe823ff509423 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 23 Oct 2025 15:42:34 +0100 Subject: [PATCH 114/118] Fix failing test Signed-off-by: Evans Mungai --- internal/logging/logging_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go index 5d354e330..75e6c4025 100644 --- a/internal/logging/logging_test.go +++ b/internal/logging/logging_test.go @@ -44,7 +44,7 @@ func TestLogHolder_Logger(t *testing.T) { holder := &LogHolder{} logger := holder.Logger() - assert.Equal(t, slog.Default().Handler(), logger.Handler()) + assert.Equal(t, slog.Handler(slog.DiscardHandler), logger.Handler()) }) } From 2ddeb50fa61c0d0617dd6f130d0c6a43dd5e89f8 Mon Sep 17 00:00:00 2001 From: Evans Mungai Date: Thu, 23 Oct 2025 18:09:58 +0100 Subject: [PATCH 115/118] Set default logger in Configuration constructor Signed-off-by: Evans Mungai --- pkg/action/action.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/action/action.go b/pkg/action/action.go index 1104386d2..9555006be 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -129,6 +129,8 @@ func ConfigurationSetLogger(h slog.Handler) ConfigurationOption { func NewConfiguration(options ...ConfigurationOption) *Configuration { c := &Configuration{} + c.SetLogger(slog.Default().Handler()) + for _, o := range options { o(c) } From ca8eae936113de27730405d5834295ac452548ca Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Sun, 21 Sep 2025 13:43:53 -0400 Subject: [PATCH 116/118] Reproducible chart archive builds Building the same chart into an archive multiple times will have the same sha256 hash. Perviously, the time in the headers for a file was time.Now() which changed each time. The time is now collected from the operating system when the file is loaded and this time is used. Fixes: #3612 Signed-off-by: Matt Farina --- internal/chart/v3/chart.go | 5 + internal/chart/v3/chart_test.go | 68 ++++--- internal/chart/v3/lint/rules/template_test.go | 22 ++- internal/chart/v3/loader/directory.go | 2 +- internal/chart/v3/loader/load.go | 17 +- internal/chart/v3/loader/load_test.go | 48 +++-- internal/chart/v3/util/create.go | 2 +- internal/chart/v3/util/save.go | 16 +- internal/chart/v3/util/save_test.go | 108 ++++++++++- pkg/action/action_test.go | 36 ++-- pkg/action/hooks_test.go | 10 +- pkg/action/install_test.go | 5 +- pkg/action/show_test.go | 30 ++-- pkg/chart/common/file.go | 4 + pkg/chart/common/util/values_test.go | 3 +- pkg/chart/loader/archive/archive.go | 8 +- pkg/chart/v2/chart.go | 5 + pkg/chart/v2/chart_test.go | 68 ++++--- pkg/chart/v2/lint/rules/template_test.go | 22 ++- pkg/chart/v2/loader/directory.go | 2 +- pkg/chart/v2/loader/load.go | 16 +- pkg/chart/v2/loader/load_test.go | 53 +++--- pkg/chart/v2/util/create.go | 2 +- pkg/chart/v2/util/save.go | 16 +- pkg/chart/v2/util/save_test.go | 107 ++++++++++- pkg/cmd/upgrade_test.go | 6 +- pkg/engine/engine_test.go | 170 ++++++++++-------- pkg/release/v1/mock.go | 2 +- 28 files changed, 586 insertions(+), 267 deletions(-) diff --git a/internal/chart/v3/chart.go b/internal/chart/v3/chart.go index 2edc6c339..48f006e79 100644 --- a/internal/chart/v3/chart.go +++ b/internal/chart/v3/chart.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "helm.sh/helm/v4/pkg/chart/common" ) @@ -47,9 +48,13 @@ type Chart struct { Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` + // SchemaModTime the schema was last modified + SchemaModTime time.Time `json:"schemamodtime,omitempty"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. Files []*common.File `json:"files"` + // ModTime the chart metadata was last modified + ModTime time.Time `json:"modtime,omitzero"` parent *Chart dependencies []*Chart diff --git a/internal/chart/v3/chart_test.go b/internal/chart/v3/chart_test.go index b1820ac0a..07cbf4b39 100644 --- a/internal/chart/v3/chart_test.go +++ b/internal/chart/v3/chart_test.go @@ -18,6 +18,7 @@ package v3 import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" @@ -25,27 +26,33 @@ import ( ) func TestCRDs(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ Raw: []*common.File{ { - Name: "fhqwhgads.yaml", - Data: []byte("Everybody to the Limit"), + Name: "fhqwhgads.yaml", + ModTime: time.Now(), + Data: []byte("Everybody to the Limit"), }, }, } @@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) { } func TestCRDObjects(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", File: &common.File{ - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", File: &common.File{ - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, } diff --git a/internal/chart/v3/lint/rules/template_test.go b/internal/chart/v3/lint/rules/template_test.go index d7665211a..0ffc92002 100644 --- a/internal/chart/v3/lint/rules/template_test.go +++ b/internal/chart/v3/lint/rules/template_test.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" "testing" + "time" chart "helm.sh/helm/v4/internal/chart/v3" "helm.sh/helm/v4/internal/chart/v3/lint/support" @@ -183,6 +184,7 @@ func TestValidateMetadataName(t *testing.T) { } func TestDeprecatedAPIFails(t *testing.T) { + modTime := time.Now() mychart := chart.Chart{ Metadata: &chart.Metadata{ APIVersion: "v2", @@ -192,12 +194,14 @@ func TestDeprecatedAPIFails(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/baddeployment.yaml", - Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + Name: "templates/baddeployment.yaml", + ModTime: modTime, + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), }, { - Name: "templates/goodsecret.yaml", - Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + Name: "templates/goodsecret.yaml", + ModTime: modTime, + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), }, }, } @@ -252,8 +256,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/configmap.yaml", - Data: []byte(manifest), + Name: "templates/configmap.yaml", + ModTime: time.Now(), + Data: []byte(manifest), }, }, } @@ -381,8 +386,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/empty-with-comments.yaml", - Data: []byte("#@formatter:off\n"), + Name: "templates/empty-with-comments.yaml", + ModTime: time.Now(), + Data: []byte("#@formatter:off\n"), }, }, } diff --git a/internal/chart/v3/loader/directory.go b/internal/chart/v3/loader/directory.go index 8cb7323dc..dfe3af3b2 100644 --- a/internal/chart/v3/loader/directory.go +++ b/internal/chart/v3/loader/directory.go @@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) { data = bytes.TrimPrefix(data, utf8bom) - files = append(files, &archive.BufferedFile{Name: n, Data: data}) + files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data}) return nil } if err = sympath.Walk(topdir, walk); err != nil { diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index b1b4bba8f..1c5b4cad1 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -25,6 +25,7 @@ import ( "maps" "os" "path/filepath" + "slices" "strings" utilyaml "k8s.io/apimachinery/pkg/util/yaml" @@ -71,11 +72,12 @@ func Load(name string) (*chart.Chart, error) { func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c := new(chart.Chart) subcharts := make(map[string][]*archive.BufferedFile) + var subChartsKeys []string // 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, &common.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -89,6 +91,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { if c.Metadata.APIVersion == "" { c.Metadata.APIVersion = chart.APIVersionV3 } + c.ModTime = f.ModTime } } for _, f := range files { @@ -109,20 +112,24 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c.Values = values case f.Name == "values.schema.json": c.Schema = f.Data + c.SchemaModTime = f.ModTime case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data, ModTime: f.ModTime}) continue } fname := strings.TrimPrefix(f.Name, "charts/") cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data}) + if slices.Index(subChartsKeys, cname) == -1 { + subChartsKeys = append(subChartsKeys, cname) + } + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data}) default: - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } } diff --git a/internal/chart/v3/loader/load_test.go b/internal/chart/v3/loader/load_test.go index 9f41429cc..f91005bf6 100644 --- a/internal/chart/v3/loader/load_test.go +++ b/internal/chart/v3/loader/load_test.go @@ -184,9 +184,11 @@ func TestLoadFile(t *testing.T) { } func TestLoadFiles(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v3 name: frobnitz description: This is a frobnitz. @@ -207,20 +209,24 @@ icon: https://example.com/64x64.png `), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "values.schema.json", - Data: []byte("type: Values"), + Name: "values.schema.json", + ModTime: modTime, + Data: []byte("type: Values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, } @@ -260,26 +266,32 @@ icon: https://example.com/64x64.png // Test the order of file loading. The Chart.yaml file needs to come first for // later comparison checks. See https://github.com/helm/helm/pull/8948 func TestLoadFilesOrder(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "requirements.yaml", - Data: []byte("dependencies:"), + Name: "requirements.yaml", + ModTime: modTime, + Data: []byte("dependencies:"), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v3 name: frobnitz description: This is a frobnitz. diff --git a/internal/chart/v3/util/create.go b/internal/chart/v3/util/create.go index c5e728721..0dfa30995 100644 --- a/internal/chart/v3/util/create.go +++ b/internal/chart/v3/util/create.go @@ -661,7 +661,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData}) } schart.Templates = updatedTemplates diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go index 49d93bf40..f755300ba 100644 --- a/internal/chart/v3/util/save.go +++ b/internal/chart/v3/util/save.go @@ -166,7 +166,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil { + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil { return err } @@ -176,7 +176,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil { return err } } @@ -184,7 +184,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save values.yaml for _, f := range c.Raw { if f.Name == ValuesfileName { - if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil { return err } } @@ -195,7 +195,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if !json.Valid(c.Schema) { return errors.New("invalid JSON in " + SchemafileName) } - if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil { + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil { return err } } @@ -203,7 +203,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save templates for _, f := range c.Templates { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -211,7 +211,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save files for _, f := range c.Files { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -226,13 +226,13 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { } // writeToTar writes a single file to a tar archive. -func writeToTar(out *tar.Writer, name string, body []byte) error { +func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error { // TODO: Do we need to create dummy parent directory names if none exist? h := &tar.Header{ Name: filepath.ToSlash(name), Mode: 0644, Size: int64(len(body)), - ModTime: time.Now(), + ModTime: modTime, } if err := out.WriteHeader(h); err != nil { return err diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 9b1b14a4c..93da34470 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -20,6 +20,8 @@ import ( "archive/tar" "bytes" "compress/gzip" + "crypto/sha256" + "fmt" "io" "os" "path" @@ -49,7 +51,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), } @@ -115,7 +117,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, } _, err := Save(c, tmp) @@ -141,7 +143,6 @@ func TestSavePreservesTimestamps(t *testing.T) { // 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{ @@ -150,14 +151,16 @@ func TestSavePreservesTimestamps(t *testing.T) { Name: "ahab", Version: "1.2.3", }, + ModTime: initialCreateTime, Values: map[string]interface{}{ "imageName": "testimage", "imageId": 42, }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")}, }, - Schema: []byte("{\n \"title\": \"Values\"\n}"), + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: initialCreateTime, } where, err := Save(c, tmp) @@ -170,8 +173,9 @@ func TestSavePreservesTimestamps(t *testing.T) { t.Fatalf("Failed to parse tar: %v", err) } + roundedTime := initialCreateTime.Round(time.Second) for _, header := range allHeaders { - if header.ModTime.Before(initialCreateTime) { + if !header.ModTime.Equal(roundedTime) { t.Fatalf("File timestamp not preserved: %v", header.ModTime) } } @@ -213,6 +217,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { func TestSaveDir(t *testing.T) { tmp := t.TempDir() + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ @@ -221,10 +226,10 @@ func TestSaveDir(t *testing.T) { Version: "1.2.3", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, }, Templates: []*common.File{ - {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")}, }, } @@ -260,3 +265,90 @@ func TestSaveDir(t *testing.T) { t.Fatalf("Did not get expected error for chart named %q", c.Name()) } } + +func TestRepeatableSave(t *testing.T) { + tmp := t.TempDir() + defer os.RemoveAll(tmp) + modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC) + tests := []struct { + name string + chart *chart.Chart + want string + }{ + { + name: "Package 1 file", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "bcb52ba7b7c2801be84cdc96d395f00749896a4679a7c9deacdfe934d0c49c1b", + }, + { + name: "Package 2 files", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV3, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + {Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "566bb87d0a044828e1e3acc4e9849b2c378eb9156a8662ceb618ea41b279bb10", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create package + dest := path.Join(tmp, "newdir") + where, err := Save(test.chart, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + // get shasum for package + result, err := sha256Sum(where) + if err != nil { + t.Fatalf("Failed to check shasum: %s", err) + } + // assert that the package SHA is what we wanted. + if result != test.want { + t.Errorf("FormatName() result = %v, want %v", result, test.want) + } + }) + } +} + +func sha256Sum(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index 8f9159dd1..b77a462f5 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -123,9 +123,10 @@ type chartOptions struct { type chartOption func(*chartOptions) func buildChart(opts ...chartOption) *chart.Chart { + modTime := time.Now() defaultTemplates := []*common.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifestWithHook)}, + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifestWithHook)}, } return buildChartWithTemplates(defaultTemplates, opts...) } @@ -181,8 +182,9 @@ func withValues(values map[string]interface{}) chartOption { func withNotes(notes string) chartOption { return func(opts *chartOptions) { opts.Templates = append(opts.Templates, &common.File{ - Name: "templates/NOTES.txt", - Data: []byte(notes), + Name: "templates/NOTES.txt", + ModTime: time.Now(), + Data: []byte(notes), }) } } @@ -201,12 +203,13 @@ func withMetadataDependency(dependency chart.Dependency) chartOption { func withSampleTemplates() chartOption { return func(opts *chartOptions) { + modTime := time.Now() sampleTemplates := []*common.File{ // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")}, + {Name: "templates/empty", ModTime: modTime, Data: []byte("")}, + {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -214,20 +217,21 @@ func withSampleTemplates() chartOption { func withSampleSecret() chartOption { return func(opts *chartOptions) { - sampleSecret := &common.File{Name: "templates/secret.yaml", Data: []byte("apiVersion: v1\nkind: Secret\n")} + sampleSecret := &common.File{Name: "templates/secret.yaml", ModTime: time.Now(), Data: []byte("apiVersion: v1\nkind: Secret\n")} opts.Templates = append(opts.Templates, sampleSecret) } } func withSampleIncludingIncorrectTemplates() chartOption { return func(opts *chartOptions) { + modTime := time.Now() sampleTemplates := []*common.File{ // This adds basic templates and partials. - {Name: "templates/goodbye", Data: []byte("goodbye: world")}, - {Name: "templates/empty", Data: []byte("")}, - {Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")}, - {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, - {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + {Name: "templates/goodbye", ModTime: modTime, Data: []byte("goodbye: world")}, + {Name: "templates/empty", ModTime: modTime, Data: []byte("")}, + {Name: "templates/incorrect", ModTime: modTime, Data: []byte("{{ .Values.bad.doh }}")}, + {Name: "templates/with-partials", ModTime: modTime, Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", ModTime: modTime, Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -236,7 +240,7 @@ func withSampleIncludingIncorrectTemplates() chartOption { func withMultipleManifestTemplate() chartOption { return func(opts *chartOptions) { sampleTemplates := []*common.File{ - {Name: "templates/rbac", Data: []byte(rbacManifests)}, + {Name: "templates/rbac", ModTime: time.Now(), Data: []byte(rbacManifests)}, } opts.Templates = append(opts.Templates, sampleTemplates...) } @@ -853,7 +857,7 @@ func TestRenderResources_PostRenderer_MergeError(t *testing.T) { Version: "0.1.0", }, Templates: []*common.File{ - {Name: "templates/invalid", Data: []byte("invalid: yaml: content:")}, + {Name: "templates/invalid", ModTime: time.Now(), Data: []byte("invalid: yaml: content:")}, }, } values := map[string]interface{}{} diff --git a/pkg/action/hooks_test.go b/pkg/action/hooks_test.go index 9502737d7..710f6a61a 100644 --- a/pkg/action/hooks_test.go +++ b/pkg/action/hooks_test.go @@ -180,9 +180,10 @@ func runInstallForHooksWithSuccess(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} instAction.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} + modTime := time.Now() templates := []*common.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifest)}, + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)}, } vals := map[string]interface{}{} @@ -209,9 +210,10 @@ func runInstallForHooksWithFailure(t *testing.T, manifest, expectedNamespace str outBuffer := &bytes.Buffer{} failingClient.PrintingKubeClient = kubefake.PrintingKubeClient{Out: io.Discard, LogOutput: outBuffer} + modTime := time.Now() templates := []*common.File{ - {Name: "templates/hello", Data: []byte("hello: world")}, - {Name: "templates/hooks", Data: []byte(manifest)}, + {Name: "templates/hello", ModTime: modTime, Data: []byte("hello: world")}, + {Name: "templates/hooks", ModTime: modTime, Data: []byte(manifest)}, } vals := map[string]interface{}{} diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index 3900c0633..9f04f40d4 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -465,8 +465,9 @@ func TestInstallRelease_DryRun_Lookup(t *testing.T) { mockChart := buildChart(withSampleTemplates()) mockChart.Templates = append(mockChart.Templates, &common.File{ - Name: "templates/lookup", - Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), + Name: "templates/lookup", + ModTime: time.Now(), + Data: []byte(`goodbye: {{ lookup "v1" "Namespace" "" "___" }}`), }) resi, err := instAction.Run(mockChart, vals) diff --git a/pkg/action/show_test.go b/pkg/action/show_test.go index faf306f2a..f3b767fca 100644 --- a/pkg/action/show_test.go +++ b/pkg/action/show_test.go @@ -18,6 +18,7 @@ package action import ( "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -26,17 +27,18 @@ import ( func TestShow(t *testing.T) { config := actionConfigFixture(t) client := NewShow(ShowAll, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*common.File{ - {Name: "README.md", Data: []byte("README\n")}, - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, - {Name: "crds/baz.yaml", Data: []byte("baz\n")}, + {Name: "README.md", ModTime: modTime, Data: []byte("README\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, + {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")}, }, Raw: []*common.File{ - {Name: "values.yaml", Data: []byte("VALUES\n")}, + {Name: "values.yaml", ModTime: modTime, Data: []byte("VALUES\n")}, }, Values: map[string]interface{}{}, } @@ -104,13 +106,14 @@ func TestShowValuesByJsonPathFormat(t *testing.T) { func TestShowCRDs(t *testing.T) { config := actionConfigFixture(t) client := NewShow(ShowCRDs, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*common.File{ - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, - {Name: "crds/baz.yaml", Data: []byte("baz\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, + {Name: "crds/baz.yaml", ModTime: modTime, Data: []byte("baz\n")}, }, } @@ -137,12 +140,13 @@ baz func TestShowNoReadme(t *testing.T) { config := actionConfigFixture(t) client := NewShow(ShowAll, config) + modTime := time.Now() client.chart = &chart.Chart{ Metadata: &chart.Metadata{Name: "alpine"}, Files: []*common.File{ - {Name: "crds/ignoreme.txt", Data: []byte("error")}, - {Name: "crds/foo.yaml", Data: []byte("---\nfoo\n")}, - {Name: "crds/bar.json", Data: []byte("---\nbar\n")}, + {Name: "crds/ignoreme.txt", ModTime: modTime, Data: []byte("error")}, + {Name: "crds/foo.yaml", ModTime: modTime, Data: []byte("---\nfoo\n")}, + {Name: "crds/bar.json", ModTime: modTime, Data: []byte("---\nbar\n")}, }, } diff --git a/pkg/chart/common/file.go b/pkg/chart/common/file.go index 304643f1a..1068bf450 100644 --- a/pkg/chart/common/file.go +++ b/pkg/chart/common/file.go @@ -15,6 +15,8 @@ limitations under the License. package common +import "time" + // File represents a file as a name/value pair. // // By convention, name is a relative path within the scope of the chart's @@ -24,4 +26,6 @@ type File struct { Name string `json:"name"` // Data is the template as byte data. Data []byte `json:"data"` + // ModTime is the file's mod-time + ModTime time.Time `json:"modtime,omitzero"` } diff --git a/pkg/chart/common/util/values_test.go b/pkg/chart/common/util/values_test.go index 5fc030567..706d3cfda 100644 --- a/pkg/chart/common/util/values_test.go +++ b/pkg/chart/common/util/values_test.go @@ -18,6 +18,7 @@ package util import ( "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -46,7 +47,7 @@ func TestToRenderValues(t *testing.T) { Templates: []*common.File{}, Values: chartValues, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, } c.AddDependency(&chart.Chart{ diff --git a/pkg/chart/loader/archive/archive.go b/pkg/chart/loader/archive/archive.go index 4d4ca4391..c6875db3f 100644 --- a/pkg/chart/loader/archive/archive.go +++ b/pkg/chart/loader/archive/archive.go @@ -29,6 +29,7 @@ import ( "path" "regexp" "strings" + "time" ) // MaxDecompressedChartSize is the maximum size of a chart archive that will be @@ -46,8 +47,9 @@ var utf8bom = []byte{0xEF, 0xBB, 0xBF} // BufferedFile represents an archive file buffered for later processing. type BufferedFile struct { - Name string - Data []byte + Name string + ModTime time.Time + Data []byte } // LoadArchiveFiles reads in files out of an archive into memory. This function @@ -148,7 +150,7 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { data := bytes.TrimPrefix(b.Bytes(), utf8bom) - files = append(files, &BufferedFile{Name: n, Data: data}) + files = append(files, &BufferedFile{Name: n, ModTime: hd.ModTime, Data: data}) b.Reset() } diff --git a/pkg/chart/v2/chart.go b/pkg/chart/v2/chart.go index f59bcd8b3..d77a53ddc 100644 --- a/pkg/chart/v2/chart.go +++ b/pkg/chart/v2/chart.go @@ -19,6 +19,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "helm.sh/helm/v4/pkg/chart/common" ) @@ -50,9 +51,13 @@ type Chart struct { Values map[string]interface{} `json:"values"` // Schema is an optional JSON schema for imposing structure on Values Schema []byte `json:"schema"` + // SchemaModTime the schema was last modified + SchemaModTime time.Time `json:"schemamodtime,omitempty"` // Files are miscellaneous files in a chart archive, // e.g. README, LICENSE, etc. Files []*common.File `json:"files"` + // ModTime the chart metadata was last modified + ModTime time.Time `json:"modtime,omitzero"` parent *Chart dependencies []*Chart diff --git a/pkg/chart/v2/chart_test.go b/pkg/chart/v2/chart_test.go index a96d8c0c0..d0837eb16 100644 --- a/pkg/chart/v2/chart_test.go +++ b/pkg/chart/v2/chart_test.go @@ -18,6 +18,7 @@ package v2 import ( "encoding/json" "testing" + "time" "github.com/stretchr/testify/assert" @@ -25,27 +26,33 @@ import ( ) func TestCRDs(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -61,8 +68,9 @@ func TestSaveChartNoRawData(t *testing.T) { chrt := Chart{ Raw: []*common.File{ { - Name: "fhqwhgads.yaml", - Data: []byte("Everybody to the Limit"), + Name: "fhqwhgads.yaml", + ModTime: time.Now(), + Data: []byte("Everybody to the Limit"), }, }, } @@ -163,27 +171,33 @@ func TestChartFullPath(t *testing.T) { } func TestCRDObjects(t *testing.T) { + modTime := time.Now() chrt := Chart{ Files: []*common.File{ { - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "bar.yaml", - Data: []byte("hello"), + Name: "bar.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crdsfoo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crdsfoo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, { - Name: "crds/README.md", - Data: []byte("# hello"), + Name: "crds/README.md", + ModTime: modTime, + Data: []byte("# hello"), }, }, } @@ -193,16 +207,18 @@ func TestCRDObjects(t *testing.T) { Name: "crds/foo.yaml", Filename: "crds/foo.yaml", File: &common.File{ - Name: "crds/foo.yaml", - Data: []byte("hello"), + Name: "crds/foo.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, { Name: "crds/foo/bar/baz.yaml", Filename: "crds/foo/bar/baz.yaml", File: &common.File{ - Name: "crds/foo/bar/baz.yaml", - Data: []byte("hello"), + Name: "crds/foo/bar/baz.yaml", + ModTime: modTime, + Data: []byte("hello"), }, }, } diff --git a/pkg/chart/v2/lint/rules/template_test.go b/pkg/chart/v2/lint/rules/template_test.go index 7f9899070..c08ba6cc3 100644 --- a/pkg/chart/v2/lint/rules/template_test.go +++ b/pkg/chart/v2/lint/rules/template_test.go @@ -22,6 +22,7 @@ import ( "path/filepath" "strings" "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -194,6 +195,7 @@ func TestValidateMetadataName(t *testing.T) { } func TestDeprecatedAPIFails(t *testing.T) { + modTime := time.Now() mychart := chart.Chart{ Metadata: &chart.Metadata{ APIVersion: "v2", @@ -203,12 +205,14 @@ func TestDeprecatedAPIFails(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/baddeployment.yaml", - Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), + Name: "templates/baddeployment.yaml", + ModTime: modTime, + Data: []byte("apiVersion: apps/v1beta1\nkind: Deployment\nmetadata:\n name: baddep\nspec: {selector: {matchLabels: {foo: bar}}}"), }, { - Name: "templates/goodsecret.yaml", - Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), + Name: "templates/goodsecret.yaml", + ModTime: modTime, + Data: []byte("apiVersion: v1\nkind: Secret\nmetadata:\n name: goodsecret"), }, }, } @@ -267,8 +271,9 @@ func TestStrictTemplateParsingMapError(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/configmap.yaml", - Data: []byte(manifest), + Name: "templates/configmap.yaml", + ModTime: time.Now(), + Data: []byte(manifest), }, }, } @@ -400,8 +405,9 @@ func TestEmptyWithCommentsManifests(t *testing.T) { }, Templates: []*common.File{ { - Name: "templates/empty-with-comments.yaml", - Data: []byte("#@formatter:off\n"), + Name: "templates/empty-with-comments.yaml", + ModTime: time.Now(), + Data: []byte("#@formatter:off\n"), }, }, } diff --git a/pkg/chart/v2/loader/directory.go b/pkg/chart/v2/loader/directory.go index c6f31560c..82578d924 100644 --- a/pkg/chart/v2/loader/directory.go +++ b/pkg/chart/v2/loader/directory.go @@ -111,7 +111,7 @@ func LoadDir(dir string) (*chart.Chart, error) { data = bytes.TrimPrefix(data, utf8bom) - files = append(files, &archive.BufferedFile{Name: n, Data: data}) + files = append(files, &archive.BufferedFile{Name: n, ModTime: fi.ModTime(), Data: data}) return nil } if err = sympath.Walk(topdir, walk); err != nil { diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index 028d59e82..ba3a9b6bc 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -76,7 +76,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { // 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, &common.File{Name: f.Name, Data: f.Data}) + c.Raw = append(c.Raw, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) if f.Name == "Chart.yaml" { if c.Metadata == nil { c.Metadata = new(chart.Metadata) @@ -90,6 +90,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { if c.Metadata.APIVersion == "" { c.Metadata.APIVersion = chart.APIVersionV1 } + c.ModTime = f.ModTime } } for _, f := range files { @@ -110,6 +111,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { c.Values = values case f.Name == "values.schema.json": c.Schema = f.Data + c.SchemaModTime = f.ModTime // Deprecated: requirements.yaml is deprecated use Chart.yaml. // We will handle it for you because we are nice people @@ -124,7 +126,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { return c, fmt.Errorf("cannot load requirements.yaml: %w", err) } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } // Deprecated: requirements.lock is deprecated use Chart.lock. case f.Name == "requirements.lock": @@ -139,22 +141,22 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { log.Printf("Warning: Dependency locking is handled in Chart.lock since apiVersion \"v2\". We recommend migrating to Chart.lock.") } if c.Metadata.APIVersion == chart.APIVersionV1 { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } case strings.HasPrefix(f.Name, "templates/"): - c.Templates = append(c.Templates, &common.File{Name: f.Name, Data: f.Data}) + c.Templates = append(c.Templates, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) case strings.HasPrefix(f.Name, "charts/"): if filepath.Ext(f.Name) == ".prov" { - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) continue } fname := strings.TrimPrefix(f.Name, "charts/") cname := strings.SplitN(fname, "/", 2)[0] - subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, Data: f.Data}) + subcharts[cname] = append(subcharts[cname], &archive.BufferedFile{Name: fname, ModTime: f.ModTime, Data: f.Data}) default: - c.Files = append(c.Files, &common.File{Name: f.Name, Data: f.Data}) + c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, Data: f.Data}) } } diff --git a/pkg/chart/v2/loader/load_test.go b/pkg/chart/v2/loader/load_test.go index 7eca5f402..ee0be5b18 100644 --- a/pkg/chart/v2/loader/load_test.go +++ b/pkg/chart/v2/loader/load_test.go @@ -219,8 +219,9 @@ func TestLoadFiles_BadCases(t *testing.T) { name: "These files contain only requirements.lock", bufferedFiles: []*archive.BufferedFile{ { - Name: "requirements.lock", - Data: []byte(""), + Name: "requirements.lock", + ModTime: time.Now(), + Data: []byte(""), }, }, expectError: "validation: chart.metadata.apiVersion is required"}, @@ -236,9 +237,11 @@ func TestLoadFiles_BadCases(t *testing.T) { } func TestLoadFiles(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v1 name: frobnitz description: This is a frobnitz. @@ -259,20 +262,24 @@ icon: https://example.com/64x64.png `), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "values.schema.json", - Data: []byte("type: Values"), + Name: "values.schema.json", + ModTime: modTime, + Data: []byte("type: Values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, } @@ -312,26 +319,32 @@ icon: https://example.com/64x64.png // Test the order of file loading. The Chart.yaml file needs to come first for // later comparison checks. See https://github.com/helm/helm/pull/8948 func TestLoadFilesOrder(t *testing.T) { + modTime := time.Now() goodFiles := []*archive.BufferedFile{ { - Name: "requirements.yaml", - Data: []byte("dependencies:"), + Name: "requirements.yaml", + ModTime: modTime, + Data: []byte("dependencies:"), }, { - Name: "values.yaml", - Data: []byte("var: some values"), + Name: "values.yaml", + ModTime: modTime, + Data: []byte("var: some values"), }, { - Name: "templates/deployment.yaml", - Data: []byte("some deployment"), + Name: "templates/deployment.yaml", + ModTime: modTime, + Data: []byte("some deployment"), }, { - Name: "templates/service.yaml", - Data: []byte("some service"), + Name: "templates/service.yaml", + ModTime: modTime, + Data: []byte("some service"), }, { - Name: "Chart.yaml", + Name: "Chart.yaml", + ModTime: modTime, Data: []byte(`apiVersion: v1 name: frobnitz description: This is a frobnitz. diff --git a/pkg/chart/v2/util/create.go b/pkg/chart/v2/util/create.go index d7c1fe31c..bf572c707 100644 --- a/pkg/chart/v2/util/create.go +++ b/pkg/chart/v2/util/create.go @@ -660,7 +660,7 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error { for _, template := range schart.Templates { newData := transform(string(template.Data), schart.Name()) - updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, Data: newData}) + updatedTemplates = append(updatedTemplates, &common.File{Name: template.Name, ModTime: template.ModTime, Data: newData}) } schart.Templates = updatedTemplates diff --git a/pkg/chart/v2/util/save.go b/pkg/chart/v2/util/save.go index 69a98924c..632588b68 100644 --- a/pkg/chart/v2/util/save.go +++ b/pkg/chart/v2/util/save.go @@ -175,7 +175,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata); err != nil { + if err := writeToTar(out, filepath.Join(base, ChartfileName), cdata, c.ModTime); err != nil { return err } @@ -187,7 +187,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if err != nil { return err } - if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata, c.Lock.Generated); err != nil { return err } } @@ -196,7 +196,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save values.yaml for _, f := range c.Raw { if f.Name == ValuesfileName { - if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data); err != nil { + if err := writeToTar(out, filepath.Join(base, ValuesfileName), f.Data, f.ModTime); err != nil { return err } } @@ -207,7 +207,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { if !json.Valid(c.Schema) { return errors.New("invalid JSON in " + SchemafileName) } - if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema); err != nil { + if err := writeToTar(out, filepath.Join(base, SchemafileName), c.Schema, c.SchemaModTime); err != nil { return err } } @@ -215,7 +215,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save templates for _, f := range c.Templates { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -223,7 +223,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { // Save files for _, f := range c.Files { n := filepath.Join(base, f.Name) - if err := writeToTar(out, n, f.Data); err != nil { + if err := writeToTar(out, n, f.Data, f.ModTime); err != nil { return err } } @@ -238,13 +238,13 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { } // writeToTar writes a single file to a tar archive. -func writeToTar(out *tar.Writer, name string, body []byte) error { +func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) error { // TODO: Do we need to create dummy parent directory names if none exist? h := &tar.Header{ Name: filepath.ToSlash(name), Mode: 0644, Size: int64(len(body)), - ModTime: time.Now(), + ModTime: modTime, } if err := out.WriteHeader(h); err != nil { return err diff --git a/pkg/chart/v2/util/save_test.go b/pkg/chart/v2/util/save_test.go index ef822a82a..5dc36e786 100644 --- a/pkg/chart/v2/util/save_test.go +++ b/pkg/chart/v2/util/save_test.go @@ -20,6 +20,8 @@ import ( "archive/tar" "bytes" "compress/gzip" + "crypto/sha256" + "fmt" "io" "os" "path" @@ -49,7 +51,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, Schema: []byte("{\n \"title\": \"Values\"\n}"), } @@ -118,7 +120,7 @@ func TestSave(t *testing.T) { Digest: "testdigest", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: time.Now(), Data: []byte("1,001 Nights")}, }, } _, err := Save(c, tmp) @@ -153,14 +155,16 @@ func TestSavePreservesTimestamps(t *testing.T) { Name: "ahab", Version: "1.2.3", }, + ModTime: initialCreateTime, Values: map[string]interface{}{ "imageName": "testimage", "imageId": 42, }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: initialCreateTime, Data: []byte("1,001 Nights")}, }, - Schema: []byte("{\n \"title\": \"Values\"\n}"), + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: initialCreateTime, } where, err := Save(c, tmp) @@ -173,8 +177,9 @@ func TestSavePreservesTimestamps(t *testing.T) { t.Fatalf("Failed to parse tar: %v", err) } + roundedTime := initialCreateTime.Round(time.Second) for _, header := range allHeaders { - if header.ModTime.Before(initialCreateTime) { + if !header.ModTime.Equal(roundedTime) { t.Fatalf("File timestamp not preserved: %v", header.ModTime) } } @@ -217,6 +222,7 @@ func retrieveAllHeadersFromTar(path string) ([]*tar.Header, error) { func TestSaveDir(t *testing.T) { tmp := t.TempDir() + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ APIVersion: chart.APIVersionV1, @@ -224,10 +230,10 @@ func TestSaveDir(t *testing.T) { Version: "1.2.3", }, Files: []*common.File{ - {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, }, Templates: []*common.File{ - {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), Data: []byte("abc: {{ .Values.abc }}")}, + {Name: path.Join(TemplatesDir, "nested", "dir", "thing.yaml"), ModTime: modTime, Data: []byte("abc: {{ .Values.abc }}")}, }, } @@ -263,3 +269,90 @@ func TestSaveDir(t *testing.T) { t.Fatalf("Did not get expected error for chart named %q", c.Name()) } } + +func TestRepeatableSave(t *testing.T) { + tmp := t.TempDir() + defer os.RemoveAll(tmp) + modTime := time.Date(2021, 9, 1, 20, 34, 58, 651387237, time.UTC) + tests := []struct { + name string + chart *chart.Chart + want string + }{ + { + name: "Package 1 file", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "5e14a06037e5d4cb277c7b21770639d4e1a337be9ae391460e50653bac5a80ed", + }, + { + name: "Package 2 files", + chart: &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: chart.APIVersionV1, + Name: "ahab", + Version: "1.2.3", + }, + ModTime: modTime, + Lock: &chart.Lock{ + Digest: "testdigest", + }, + Files: []*common.File{ + {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, + {Name: "scheherazade/dunyazad.txt", ModTime: modTime, Data: []byte("1,001 Nights again")}, + }, + Schema: []byte("{\n \"title\": \"Values\"\n}"), + SchemaModTime: modTime, + }, + want: "6967787da46fbfcc563cad31240e564e14f2602e6f66302129a59a9669622a36", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // create package + dest := path.Join(tmp, "newdir") + where, err := Save(test.chart, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + // get shasum for package + result, err := sha256Sum(where) + if err != nil { + t.Fatalf("Failed to check shasum: %s", err) + } + // assert that the package SHA is what we wanted. + if result != test.want { + t.Errorf("FormatName() result = %v, want %v", result, test.want) + } + }) + } +} + +func sha256Sum(filePath string) (string, error) { + f, err := os.Open(filePath) + if err != nil { + return "", err + } + defer f.Close() + + h := sha256.New() + if _, err := io.Copy(h, f); err != nil { + return "", err + } + + return fmt.Sprintf("%x", h.Sum(nil)), nil +} diff --git a/pkg/cmd/upgrade_test.go b/pkg/cmd/upgrade_test.go index fd715a1fa..8729be0ec 100644 --- a/pkg/cmd/upgrade_test.go +++ b/pkg/cmd/upgrade_test.go @@ -23,6 +23,7 @@ import ( "reflect" "strings" "testing" + "time" "helm.sh/helm/v4/pkg/chart/common" chart "helm.sh/helm/v4/pkg/chart/v2" @@ -408,7 +409,7 @@ func prepareMockRelease(t *testing.T, releaseName string) (func(n string, v int, Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", ModTime: time.Now(), Data: configmapData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { @@ -513,6 +514,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri if err != nil { t.Fatalf("Error loading template yaml %v", err) } + modTime := time.Now() cfile := &chart.Chart{ Metadata: &chart.Metadata{ APIVersion: chart.APIVersionV1, @@ -520,7 +522,7 @@ func prepareMockReleaseWithSecret(t *testing.T, releaseName string) (func(n stri Description: "A Helm chart for Kubernetes", Version: "0.1.0", }, - Templates: []*common.File{{Name: "templates/configmap.yaml", Data: configmapData}, {Name: "templates/secret.yaml", Data: secretData}}, + Templates: []*common.File{{Name: "templates/configmap.yaml", ModTime: modTime, Data: configmapData}, {Name: "templates/secret.yaml", ModTime: modTime, Data: secretData}}, } chartPath := filepath.Join(tmpChart, cfile.Metadata.Name) if err := chartutil.SaveDir(cfile, tmpChart); err != nil { diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index 542ac2a9c..e541ef9d7 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -23,6 +23,7 @@ import ( "sync" "testing" "text/template" + "time" "github.com/stretchr/testify/assert" @@ -90,17 +91,18 @@ func TestFuncMap(t *testing.T) { } func TestRender(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{ Name: "moby", Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/test1", Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, - {Name: "templates/test2", Data: []byte("{{.Values.global.callme | lower }}")}, - {Name: "templates/test3", Data: []byte("{{.noValue}}")}, - {Name: "templates/test4", Data: []byte("{{toJson .Values}}")}, - {Name: "templates/test5", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + {Name: "templates/test1", ModTime: modTime, Data: []byte("{{.Values.outer | title }} {{.Values.inner | title}}")}, + {Name: "templates/test2", ModTime: modTime, Data: []byte("{{.Values.global.callme | lower }}")}, + {Name: "templates/test3", ModTime: modTime, Data: []byte("{{.noValue}}")}, + {Name: "templates/test4", ModTime: modTime, Data: []byte("{{toJson .Values}}")}, + {Name: "templates/test5", ModTime: modTime, Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{"outer": "DEFAULT", "inner": "DEFAULT"}, } @@ -140,14 +142,16 @@ func TestRender(t *testing.T) { } func TestRenderRefsOrdering(t *testing.T) { + modTime := time.Now() + parentChart := &chart.Chart{ Metadata: &chart.Metadata{ Name: "parent", Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, - {Name: "templates/test.yaml", Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, + {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}parent value{{- end -}}`)}, + {Name: "templates/test.yaml", ModTime: modTime, Data: []byte(`{{ tpl "{{ include \"test\" . }}" . }}`)}, }, } childChart := &chart.Chart{ @@ -156,7 +160,7 @@ func TestRenderRefsOrdering(t *testing.T) { Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/_helpers.tpl", Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, + {Name: "templates/_helpers.tpl", ModTime: modTime, Data: []byte(`{{- define "test" -}}child value{{- end -}}`)}, }, } parentChart.AddDependency(childChart) @@ -220,7 +224,7 @@ func TestRenderWithDNS(t *testing.T) { Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/test1", Data: []byte("{{getHostByName \"helm.sh\"}}")}, + {Name: "templates/test1", ModTime: time.Now(), Data: []byte("{{getHostByName \"helm.sh\"}}")}, }, Values: map[string]interface{}{}, } @@ -355,10 +359,12 @@ func TestRenderWithClientProvider(t *testing.T) { Values: map[string]interface{}{}, } + modTime := time.Now() for name, exp := range cases { c.Templates = append(c.Templates, &common.File{ - Name: path.Join("templates", name), - Data: []byte(exp.template), + Name: path.Join("templates", name), + ModTime: modTime, + Data: []byte(exp.template), }) } @@ -393,7 +399,7 @@ func TestRenderWithClientProvider_error(t *testing.T) { Version: "1.2.3", }, Templates: []*common.File{ - {Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, + {Name: "templates/error", ModTime: time.Now(), Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)}, }, Values: map[string]interface{}{}, } @@ -558,18 +564,19 @@ func TestFailErrors(t *testing.T) { } func TestAllTemplates(t *testing.T) { + modTime := time.Now() ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "ch1"}, Templates: []*common.File{ - {Name: "templates/foo", Data: []byte("foo")}, - {Name: "templates/bar", Data: []byte("bar")}, + {Name: "templates/foo", ModTime: modTime, Data: []byte("foo")}, + {Name: "templates/bar", ModTime: modTime, Data: []byte("bar")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "laboratory mice"}, Templates: []*common.File{ - {Name: "templates/pinky", Data: []byte("pinky")}, - {Name: "templates/brain", Data: []byte("brain")}, + {Name: "templates/pinky", ModTime: modTime, Data: []byte("pinky")}, + {Name: "templates/brain", ModTime: modTime, Data: []byte("brain")}, }, } ch1.AddDependency(dep1) @@ -577,7 +584,7 @@ func TestAllTemplates(t *testing.T) { dep2 := &chart.Chart{ Metadata: &chart.Metadata{Name: "same thing we do every night"}, Templates: []*common.File{ - {Name: "templates/innermost", Data: []byte("innermost")}, + {Name: "templates/innermost", ModTime: modTime, Data: []byte("innermost")}, }, } dep1.AddDependency(dep2) @@ -589,16 +596,17 @@ func TestAllTemplates(t *testing.T) { } func TestChartValuesContainsIsRoot(t *testing.T) { + modTime := time.Now() ch1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "parent"}, Templates: []*common.File{ - {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")}, }, } dep1 := &chart.Chart{ Metadata: &chart.Metadata{Name: "child"}, Templates: []*common.File{ - {Name: "templates/isroot", Data: []byte("{{.Chart.IsRoot}}")}, + {Name: "templates/isroot", ModTime: modTime, Data: []byte("{{.Chart.IsRoot}}")}, }, } ch1.AddDependency(dep1) @@ -621,16 +629,17 @@ func TestChartValuesContainsIsRoot(t *testing.T) { func TestRenderDependency(t *testing.T) { deptpl := `{{define "myblock"}}World{{end}}` toptpl := `Hello {{template "myblock"}}` + modTime := time.Now() ch := &chart.Chart{ Metadata: &chart.Metadata{Name: "outerchart"}, Templates: []*common.File{ - {Name: "templates/outer", Data: []byte(toptpl)}, + {Name: "templates/outer", ModTime: modTime, Data: []byte(toptpl)}, }, } ch.AddDependency(&chart.Chart{ Metadata: &chart.Metadata{Name: "innerchart"}, Templates: []*common.File{ - {Name: "templates/inner", Data: []byte(deptpl)}, + {Name: "templates/inner", ModTime: modTime, Data: []byte(deptpl)}, }, }) @@ -659,11 +668,12 @@ func TestRenderNestedValues(t *testing.T) { // Ensure subcharts scopes are working. subchartspath := "templates/subcharts.tpl" + modTime := time.Now() deepest := &chart.Chart{ Metadata: &chart.Metadata{Name: "deepest"}, Templates: []*common.File{ - {Name: deepestpath, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, - {Name: checkrelease, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, + {Name: deepestpath, ModTime: modTime, Data: []byte(`And this same {{.Values.what}} that smiles {{.Values.global.when}}`)}, + {Name: checkrelease, ModTime: modTime, Data: []byte(`Tomorrow will be {{default "happy" .Release.Name }}`)}, }, Values: map[string]interface{}{"what": "milkshake", "where": "here"}, } @@ -671,7 +681,7 @@ func TestRenderNestedValues(t *testing.T) { inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "herrick"}, Templates: []*common.File{ - {Name: innerpath, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, + {Name: innerpath, ModTime: modTime, Data: []byte(`Old {{.Values.who}} is still a-flyin'`)}, }, Values: map[string]interface{}{"who": "Robert", "what": "glasses"}, } @@ -680,8 +690,8 @@ func TestRenderNestedValues(t *testing.T) { outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "top"}, Templates: []*common.File{ - {Name: outerpath, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, - {Name: subchartspath, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, + {Name: outerpath, ModTime: modTime, Data: []byte(`Gather ye {{.Values.what}} while ye may`)}, + {Name: subchartspath, ModTime: modTime, Data: []byte(`The glorious Lamp of {{.Subcharts.herrick.Subcharts.deepest.Values.where}}, the {{.Subcharts.herrick.Values.what}}`)}, }, Values: map[string]interface{}{ "what": "stinkweed", @@ -754,23 +764,24 @@ func TestRenderNestedValues(t *testing.T) { } func TestRenderBuiltinValues(t *testing.T) { + modTime := time.Now() inner := &chart.Chart{ Metadata: &chart.Metadata{Name: "Latium", APIVersion: chart.APIVersionV2}, Templates: []*common.File{ - {Name: "templates/Lavinia", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "templates/From", Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, + {Name: "templates/Lavinia", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/From", ModTime: modTime, Data: []byte(`{{.Files.author | printf "%s"}} {{.Files.Get "book/title.txt"}}`)}, }, Files: []*common.File{ - {Name: "author", Data: []byte("Virgil")}, - {Name: "book/title.txt", Data: []byte("Aeneid")}, + {Name: "author", ModTime: modTime, Data: []byte("Virgil")}, + {Name: "book/title.txt", ModTime: modTime, Data: []byte("Aeneid")}, }, } outer := &chart.Chart{ Metadata: &chart.Metadata{Name: "Troy", APIVersion: chart.APIVersionV2}, Templates: []*common.File{ - {Name: "templates/Aeneas", Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, - {Name: "templates/Amata", Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, + {Name: "templates/Aeneas", ModTime: modTime, Data: []byte(`{{.Template.Name}}{{.Chart.Name}}{{.Release.Name}}`)}, + {Name: "templates/Amata", ModTime: modTime, Data: []byte(`{{.Subcharts.Latium.Chart.Name}} {{.Subcharts.Latium.Files.author | printf "%s"}}`)}, }, } outer.AddDependency(inner) @@ -805,11 +816,12 @@ func TestRenderBuiltinValues(t *testing.T) { } func TestAlterFuncMap_include(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conrad"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, - {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "conrad/templates/_partial" . | indent 2}} dead.`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)}, }, } @@ -817,8 +829,8 @@ func TestAlterFuncMap_include(t *testing.T) { d := &chart.Chart{ Metadata: &chart.Metadata{Name: "nested"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, - {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Release.Name}} - he`)}, }, } @@ -848,11 +860,12 @@ func TestAlterFuncMap_include(t *testing.T) { } func TestAlterFuncMap_require(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "conan"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, - {Name: "templates/bases", Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(`All your base are belong to {{ required "A valid 'who' is required" .Values.who }}`)}, + {Name: "templates/bases", ModTime: modTime, Data: []byte(`All {{ required "A valid 'bases' is required" .Values.bases }} of them!`)}, }, } @@ -913,7 +926,7 @@ func TestAlterFuncMap_tpl(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, + {Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value}}" .}}`)}, }, } @@ -942,7 +955,7 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, + {Name: "templates/base", ModTime: time.Now(), Data: []byte(`Evaluate tpl {{tpl "Value: {{ .Values.value | quote}}" .}}`)}, }, } @@ -968,11 +981,12 @@ func TestAlterFuncMap_tplfunc(t *testing.T) { } func TestAlterFuncMap_tplinclude(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplFunction"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, - {Name: "templates/_partial", Data: []byte(`{{.Template.Name}}`)}, + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl "{{include ` + "`" + `TplFunction/templates/_partial` + "`" + ` . | quote }}" .}}`)}, + {Name: "templates/_partial", ModTime: modTime, Data: []byte(`{{.Template.Name}}`)}, }, } v := common.Values{ @@ -998,12 +1012,14 @@ func TestAlterFuncMap_tplinclude(t *testing.T) { } func TestRenderRecursionLimit(t *testing.T) { + modTime := time.Now() + // endless recursion should produce an error c := &chart.Chart{ Metadata: &chart.Metadata{Name: "bad"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`{{include "recursion" . }}`)}, - {Name: "templates/recursion", Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{include "recursion" . }}`)}, + {Name: "templates/recursion", ModTime: modTime, Data: []byte(`{{define "recursion"}}{{include "recursion" . }}{{end}}`)}, }, } v := common.Values{ @@ -1032,8 +1048,8 @@ func TestRenderRecursionLimit(t *testing.T) { d := &chart.Chart{ Metadata: &chart.Metadata{Name: "overlook"}, Templates: []*common.File{ - {Name: "templates/quote", Data: []byte(repeatedIncl.String())}, - {Name: "templates/_function", Data: []byte(printFunc)}, + {Name: "templates/quote", ModTime: modTime, Data: []byte(repeatedIncl.String())}, + {Name: "templates/_function", ModTime: modTime, Data: []byte(printFunc)}, }, } @@ -1053,15 +1069,16 @@ func TestRenderRecursionLimit(t *testing.T) { } func TestRenderLoadTemplateForTplFromFile(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplLoadFromFile"}, Templates: []*common.File{ - {Name: "templates/base", Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, - {Name: "templates/_function", Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, + {Name: "templates/base", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename) . }}`)}, + {Name: "templates/_function", ModTime: modTime, Data: []byte(`{{define "test-function"}}test-function{{end}}`)}, }, Files: []*common.File{ - {Name: "test", Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, - {Name: "test2", Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, + {Name: "test", ModTime: modTime, Data: []byte(`{{ tpl (.Files.Get .Values.filename2) .}}`)}, + {Name: "test2", ModTime: modTime, Data: []byte(`{{include "test-function" .}}{{define "nested-define"}}nested-define-content{{end}} {{include "nested-define" .}}`)}, }, } @@ -1088,12 +1105,13 @@ func TestRenderLoadTemplateForTplFromFile(t *testing.T) { } func TestRenderTplEmpty(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplEmpty"}, Templates: []*common.File{ - {Name: "templates/empty-string", Data: []byte(`{{tpl "" .}}`)}, - {Name: "templates/empty-action", Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, - {Name: "templates/only-defines", Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, + {Name: "templates/empty-string", ModTime: modTime, Data: []byte(`{{tpl "" .}}`)}, + {Name: "templates/empty-action", ModTime: modTime, Data: []byte(`{{tpl "{{ \"\"}}" .}}`)}, + {Name: "templates/only-defines", ModTime: modTime, Data: []byte(`{{tpl "{{define \"not-invoked\"}}not-rendered{{end}}" .}}`)}, }, } v := common.Values{ @@ -1121,15 +1139,16 @@ func TestRenderTplEmpty(t *testing.T) { } func TestRenderTplTemplateNames(t *testing.T) { + modTime := time.Now() // .Template.BasePath and .Name make it through c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplTemplateNames"}, Templates: []*common.File{ - {Name: "templates/default-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, - {Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, - {Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, - {Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, - {Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, + {Name: "templates/default-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .}}`)}, + {Name: "templates/default-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)}, + {Name: "templates/modified-basepath", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)}, + {Name: "templates/modified-name", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)}, + {Name: "templates/modified-field", ModTime: modTime, Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)}, }, } v := common.Values{ @@ -1168,12 +1187,13 @@ func TestRenderTplTemplateNames(t *testing.T) { } func TestRenderTplRedefines(t *testing.T) { + modTime := time.Now() // Redefining a template inside 'tpl' does not affect the outer definition c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplRedefines"}, Templates: []*common.File{ - {Name: "templates/_partials", Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, - {Name: "templates/partial", Data: []byte( + {Name: "templates/_partials", ModTime: modTime, Data: []byte(`{{define "partial"}}original-in-partial{{end}}`)}, + {Name: "templates/partial", ModTime: modTime, Data: []byte( `before: {{include "partial" .}}\n{{tpl .Values.partialText .}}\nafter: {{include "partial" .}}`, )}, {Name: "templates/manifest", Data: []byte( @@ -1238,7 +1258,7 @@ func TestRenderTplMissingKey(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKey"}, Templates: []*common.File{ - {Name: "templates/manifest", Data: []byte( + {Name: "templates/manifest", ModTime: time.Now(), Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, @@ -1271,7 +1291,7 @@ func TestRenderTplMissingKeyString(t *testing.T) { c := &chart.Chart{ Metadata: &chart.Metadata{Name: "TplMissingKeyStrict"}, Templates: []*common.File{ - {Name: "templates/manifest", Data: []byte( + {Name: "templates/manifest", ModTime: time.Now(), Data: []byte( `missingValue: {{tpl "{{.Values.noSuchKey}}" .}}`, )}, }, @@ -1300,16 +1320,17 @@ func TestRenderTplMissingKeyString(t *testing.T) { } func TestNestedHelpersProducesMultilineStacktrace(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "NestedHelperFunctions"}, Templates: []*common.File{ - {Name: "templates/svc.yaml", Data: []byte( + {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, - {Name: "templates/_helpers_1.tpl", Data: []byte( + {Name: "templates/_helpers_1.tpl", ModTime: modTime, Data: []byte( `{{- define "nested_helper.name" -}}{{- include "common.names.get_name" . -}}{{- end -}}`, )}, - {Name: "charts/common/templates/_helpers_2.tpl", Data: []byte( + {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte( `{{- define "common.names.get_name" -}}{{- .Values.nonexistant.key | trunc 63 | trimSuffix "-" -}}{{- end -}}`, )}, }, @@ -1338,16 +1359,17 @@ NestedHelperFunctions/charts/common/templates/_helpers_2.tpl:1:49 } func TestMultilineNoTemplateAssociatedError(t *testing.T) { + modTime := time.Now() c := &chart.Chart{ Metadata: &chart.Metadata{Name: "multiline"}, Templates: []*common.File{ - {Name: "templates/svc.yaml", Data: []byte( + {Name: "templates/svc.yaml", ModTime: modTime, Data: []byte( `name: {{ include "nested_helper.name" . }}`, )}, - {Name: "templates/test.yaml", Data: []byte( + {Name: "templates/test.yaml", ModTime: modTime, Data: []byte( `{{ toYaml .Values }}`, )}, - {Name: "charts/common/templates/_helpers_2.tpl", Data: []byte( + {Name: "charts/common/templates/_helpers_2.tpl", ModTime: modTime, Data: []byte( `{{ toYaml .Values }}`, )}, }, @@ -1371,17 +1393,21 @@ template: no template "nested_helper.name" associated with template "gotpl"` } func TestRenderCustomTemplateFuncs(t *testing.T) { + modTime := time.Now() + // Create a chart with two templates that use custom functions c := &chart.Chart{ Metadata: &chart.Metadata{Name: "CustomFunc"}, Templates: []*common.File{ { - Name: "templates/manifest", - Data: []byte(`{{exclaim .Values.message}}`), + Name: "templates/manifest", + ModTime: modTime, + Data: []byte(`{{exclaim .Values.message}}`), }, { - Name: "templates/override", - Data: []byte(`{{ upper .Values.message }}`), + Name: "templates/override", + ModTime: modTime, + Data: []byte(`{{ upper .Values.message }}`), }, }, } diff --git a/pkg/release/v1/mock.go b/pkg/release/v1/mock.go index 06ad90e8f..dc135a24a 100644 --- a/pkg/release/v1/mock.go +++ b/pkg/release/v1/mock.go @@ -101,7 +101,7 @@ func Mock(opts *MockReleaseOptions) *Release { }, }, Templates: []*common.File{ - {Name: "templates/foo.tpl", Data: []byte(MockManifest)}, + {Name: "templates/foo.tpl", ModTime: time.Now(), Data: []byte(MockManifest)}, }, } } From c6334c2ac5a0d7ee997c529b7ce870ec64d99dc2 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 29 Oct 2025 14:13:17 -0400 Subject: [PATCH 117/118] Updating Go and golangci-lint versions Signed-off-by: Matt Farina --- .github/env | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/env b/.github/env index 4384ba074..9d79b174f 100644 --- a/.github/env +++ b/.github/env @@ -1,2 +1,2 @@ -GOLANG_VERSION=1.24 -GOLANGCI_LINT_VERSION=v2.1.0 +GOLANG_VERSION=1.25 +GOLANGCI_LINT_VERSION=v2.5.0 From 15300549f085edbbb395ec2845518d20e89748b1 Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 29 Oct 2025 15:11:20 -0400 Subject: [PATCH 118/118] When time not available, using time.Now Note, when time is not available, the builds are not reproducible. This problem would only happen when an SDK user is using parts of the API to build their own tooling. Helm will consistently inject the dates through the higher level APIs. Signed-off-by: Matt Farina --- internal/chart/v3/util/save.go | 3 +++ internal/chart/v3/util/save_test.go | 10 ++++++---- pkg/chart/v2/util/save.go | 3 +++ pkg/chart/v2/util/save_test.go | 14 ++++++++------ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/internal/chart/v3/util/save.go b/internal/chart/v3/util/save.go index f755300ba..f886c6175 100644 --- a/internal/chart/v3/util/save.go +++ b/internal/chart/v3/util/save.go @@ -234,6 +234,9 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er Size: int64(len(body)), ModTime: modTime, } + if h.ModTime.IsZero() { + h.ModTime = time.Now() + } if err := out.WriteHeader(h); err != nil { return err } diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 93da34470..62625919b 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -285,7 +285,8 @@ func TestRepeatableSave(t *testing.T) { }, ModTime: modTime, Lock: &chart.Lock{ - Digest: "testdigest", + Digest: "testdigest", + Generated: modTime, }, Files: []*common.File{ {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, @@ -293,7 +294,7 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "bcb52ba7b7c2801be84cdc96d395f00749896a4679a7c9deacdfe934d0c49c1b", + want: "5bfea18cc3c8cbc265744bc32bffa9489a4dbe87d6b51b90f4255e4839d35e03", }, { name: "Package 2 files", @@ -305,7 +306,8 @@ func TestRepeatableSave(t *testing.T) { }, ModTime: modTime, Lock: &chart.Lock{ - Digest: "testdigest", + Digest: "testdigest", + Generated: modTime, }, Files: []*common.File{ {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, @@ -314,7 +316,7 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "566bb87d0a044828e1e3acc4e9849b2c378eb9156a8662ceb618ea41b279bb10", + want: "a240365c21e0a2f4a57873132a9b686566a612d08bcb3f20c9446bfff005ccce", }, } for _, test := range tests { diff --git a/pkg/chart/v2/util/save.go b/pkg/chart/v2/util/save.go index 632588b68..e66d86991 100644 --- a/pkg/chart/v2/util/save.go +++ b/pkg/chart/v2/util/save.go @@ -246,6 +246,9 @@ func writeToTar(out *tar.Writer, name string, body []byte, modTime time.Time) er Size: int64(len(body)), ModTime: modTime, } + if h.ModTime.IsZero() { + h.ModTime = time.Now() + } if err := out.WriteHeader(h); err != nil { return err } diff --git a/pkg/chart/v2/util/save_test.go b/pkg/chart/v2/util/save_test.go index 5dc36e786..e317d1c09 100644 --- a/pkg/chart/v2/util/save_test.go +++ b/pkg/chart/v2/util/save_test.go @@ -283,13 +283,14 @@ func TestRepeatableSave(t *testing.T) { name: "Package 1 file", chart: &chart.Chart{ Metadata: &chart.Metadata{ - APIVersion: chart.APIVersionV1, + APIVersion: chart.APIVersionV2, Name: "ahab", Version: "1.2.3", }, ModTime: modTime, Lock: &chart.Lock{ - Digest: "testdigest", + Digest: "testdigest", + Generated: modTime, }, Files: []*common.File{ {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, @@ -297,19 +298,20 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "5e14a06037e5d4cb277c7b21770639d4e1a337be9ae391460e50653bac5a80ed", + want: "fea2662522317b65c2788ff9e5fc446a9264830038dac618d4449493d99b3257", }, { name: "Package 2 files", chart: &chart.Chart{ Metadata: &chart.Metadata{ - APIVersion: chart.APIVersionV1, + APIVersion: chart.APIVersionV2, Name: "ahab", Version: "1.2.3", }, ModTime: modTime, Lock: &chart.Lock{ - Digest: "testdigest", + Digest: "testdigest", + Generated: modTime, }, Files: []*common.File{ {Name: "scheherazade/shahryar.txt", ModTime: modTime, Data: []byte("1,001 Nights")}, @@ -318,7 +320,7 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "6967787da46fbfcc563cad31240e564e14f2602e6f66302129a59a9669622a36", + want: "7ae92b2f274bb51ea3f1969e4187d78cc52b5f6f663b44b8fb3b40bcb8ee46f3", }, } for _, test := range tests {