diff --git a/.github/env b/.github/env index 9a0c1ef4c..eb1fa0ee8 100644 --- a/.github/env +++ b/.github/env @@ -1,2 +1,2 @@ -GOLANG_VERSION=1.26 -GOLANGCI_LINT_VERSION=v2.12.2 +GOLANG_VERSION=1.27 +GOLANGCI_LINT_VERSION=v2.13.1 diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 60df8a308..42442d392 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -42,8 +42,7 @@ func main() { } if err := cmd.Execute(); err != nil { - var cerr helmcmd.CommandError - if errors.As(err, &cerr) { + if cerr, ok := errors.AsType[helmcmd.CommandError](err); ok { os.Exit(cerr.ExitCode) } os.Exit(1) diff --git a/internal/chart/v3/loader/load.go b/internal/chart/v3/loader/load.go index 0ff040eae..72343d7b6 100644 --- a/internal/chart/v3/loader/load.go +++ b/internal/chart/v3/loader/load.go @@ -123,7 +123,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { } fname := strings.TrimPrefix(f.Name, "charts/") - cname := strings.SplitN(fname, "/", 2)[0] + cname, _, _ := strings.Cut(fname, "/") if slices.Index(subChartsKeys, cname) == -1 { subChartsKeys = append(subChartsKeys, cname) } diff --git a/internal/chart/v3/util/dependencies.go b/internal/chart/v3/util/dependencies.go index e1b3e0b97..8b84ef428 100644 --- a/internal/chart/v3/util/dependencies.go +++ b/internal/chart/v3/util/dependencies.go @@ -46,7 +46,6 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, if c != "" { // retrieve value vv, err := cvals.PathValue(cpath + c) - var errNoValue common.ErrNoValue if err == nil { // if not bool, warn if bv, ok := vv.(bool); ok { @@ -54,7 +53,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, break } slog.Warn("returned non-bool value", "path", c, "chart", r.Name) - } else if errors.As(err, &errNoValue) { + } else if _, ok := errors.AsType[common.ErrNoValue](err); ok { // this is a real error slog.Warn("the method PathValue returned error", slog.Any("error", err)) } diff --git a/internal/chart/v3/util/doc.go b/internal/chart/v3/util/doc.go index dc5a07462..70dc7199f 100644 --- a/internal/chart/v3/util/doc.go +++ b/internal/chart/v3/util/doc.go @@ -42,4 +42,4 @@ into a Chart. When creating charts in memory, use the 'helm.sh/helm/pkg/chart' package directly. */ -package util // import "helm.sh/helm/v4/internal/chart/v3/util" +package util diff --git a/internal/chart/v3/util/save_test.go b/internal/chart/v3/util/save_test.go index 26bb54019..00f2c5cf4 100644 --- a/internal/chart/v3/util/save_test.go +++ b/internal/chart/v3/util/save_test.go @@ -231,13 +231,10 @@ func TestSaveDir(t *testing.T) { } 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", @@ -258,7 +255,6 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "5bfea18cc3c8cbc265744bc32bffa9489a4dbe87d6b51b90f4255e4839d35e03", }, { name: "Package 2 files", @@ -280,20 +276,26 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "a240365c21e0a2f4a57873132a9b686566a612d08bcb3f20c9446bfff005ccce", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - // create package - dest := path.Join(tmp, "newdir") - where, err := Save(test.chart, dest) + // Package the same chart twice, into separate directories so the + // second Save does not simply overwrite the first. + first, err := Save(test.chart, t.TempDir()) + require.NoError(t, err, "Failed to save") + second, err := Save(test.chart, t.TempDir()) require.NoError(t, err, "Failed to save") - // get shasum for package - result, err := sha256Sum(where) + + firstSum, err := sha256Sum(first) require.NoError(t, err, "Failed to check shasum") - // assert that the package SHA is what we wanted. - assert.Equal(t, test.want, result, "FormatName() result = %v, want %v", result, test.want) + secondSum, err := sha256Sum(second) + require.NoError(t, err, "Failed to check shasum") + + // Packaging a chart must be reproducible. The digest itself is not + // pinned here because the bytes depend on the compression + // implementation of the Go release Helm is built with. + assert.Equal(t, firstSum, secondSum, "Save() is not repeatable") }) } } diff --git a/internal/plugin/config_test.go b/internal/plugin/config_test.go index ec81ffed6..ca7b26773 100644 --- a/internal/plugin/config_test.go +++ b/internal/plugin/config_test.go @@ -41,7 +41,7 @@ func TestUnmarshaConfig(t *testing.T) { ShortHelp: "short help string", LongHelp: "long help string", IgnoreFlags: true, - }, *(config.(*schema.ConfigCLIV1))) + }, *config.(*schema.ConfigCLIV1)) } // Test unmarshalling invalid config data diff --git a/internal/plugin/runtime_subprocess.go b/internal/plugin/runtime_subprocess.go index 2dbe220d9..9ea69e116 100644 --- a/internal/plugin/runtime_subprocess.go +++ b/internal/plugin/runtime_subprocess.go @@ -74,7 +74,7 @@ func (r *RuntimeSubprocess) CreatePlugin(pluginDir string, metadata *Metadata) ( return &SubprocessPluginRuntime{ metadata: *metadata, pluginDir: pluginDir, - RuntimeConfig: *(metadata.RuntimeConfig.(*RuntimeConfigSubprocess)), + RuntimeConfig: *metadata.RuntimeConfig.(*RuntimeConfigSubprocess), EnvVars: maps.Clone(r.EnvVars), }, nil } @@ -153,8 +153,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error { slog.Debug("executing plugin hook command", slog.String("pluginName", r.metadata.Name), slog.String("command", cmd.String())) if err := cmd.Run(); err != nil { - var eerr *exec.ExitError - if errors.As(err, &eerr) { + if eerr, ok := errors.AsType[*exec.ExitError](err); ok { os.Stderr.Write(eerr.Stderr) return fmt.Errorf("plugin %s hook for %q exited with error", event, r.metadata.Name) } @@ -168,8 +167,7 @@ func (r *SubprocessPluginRuntime) InvokeHook(event string) error { // then replace the other three with a call to this func func executeCmd(prog *exec.Cmd, pluginName string) error { if err := prog.Run(); err != nil { - var eerr *exec.ExitError - if errors.As(err, &eerr) { + if eerr, ok := errors.AsType[*exec.ExitError](err); ok { slog.Debug( "plugin execution failed", slog.String("pluginName", pluginName), diff --git a/internal/plugin/runtime_subprocess_getter.go b/internal/plugin/runtime_subprocess_getter.go index 3f07fa764..67feab94f 100644 --- a/internal/plugin/runtime_subprocess_getter.go +++ b/internal/plugin/runtime_subprocess_getter.go @@ -42,7 +42,7 @@ func getProtocolCommand(commands []SubprocessProtocolCommand, protocol string) * // TODO can we replace a lot of this func with RuntimeSubprocess.invokeWithEnv? func (r *SubprocessPluginRuntime) runGetter(ctx context.Context, input *Input) (*Output, error) { - msg, ok := (input.Message).(schema.InputMessageGetterV1) + msg, ok := input.Message.(schema.InputMessageGetterV1) if !ok { return nil, fmt.Errorf("expected input type schema.InputMessageGetterV1, got %T", input) } diff --git a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go index ae62d0e6f..666e1be40 100644 --- a/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go +++ b/internal/third_party/k8s.io/kubernetes/deployment/util/deploymentutil.go @@ -136,11 +136,11 @@ func IsRollingUpdate(deployment *apps.Deployment) bool { // MaxUnavailable returns the maximum unavailable pods a rolling deployment can take. func MaxUnavailable(deployment apps.Deployment) int32 { - if !IsRollingUpdate(&deployment) || *(deployment.Spec.Replicas) == 0 { + if !IsRollingUpdate(&deployment) || *deployment.Spec.Replicas == 0 { return int32(0) } // Error caught by validation - _, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *(deployment.Spec.Replicas)) + _, maxUnavailable, _ := ResolveFenceposts(deployment.Spec.Strategy.RollingUpdate.MaxSurge, deployment.Spec.Strategy.RollingUpdate.MaxUnavailable, *deployment.Spec.Replicas) if maxUnavailable > *deployment.Spec.Replicas { return *deployment.Spec.Replicas } diff --git a/internal/version/clientgo_test.go b/internal/version/clientgo_test.go index 624c669af..2304306e0 100644 --- a/internal/version/clientgo_test.go +++ b/internal/version/clientgo_test.go @@ -17,14 +17,20 @@ limitations under the License. package version import ( + "strings" "testing" "github.com/stretchr/testify/require" ) func TestK8sClientGoModVersion(t *testing.T) { - // Unfortunately, test builds don't include debug info / module info - // So we expect "K8sIOClientGoModVersion" to return error - _, err := K8sIOClientGoModVersion() - require.ErrorContains(t, err, "k8s.io/client-go not found in build info") + // Whether module info is embedded in a test binary depends on the Go + // release: builds before Go 1.27 omit it, so the lookup fails. Accept + // either outcome, but require that a successful lookup returns a version. + v, err := K8sIOClientGoModVersion() + if err != nil { + require.ErrorContains(t, err, "k8s.io/client-go not found in build info") + return + } + require.True(t, strings.HasPrefix(v, "v"), "expected a semver-like version, got %q", v) } diff --git a/pkg/chart/v2/loader/load.go b/pkg/chart/v2/loader/load.go index d7b125b9b..feaa28f12 100644 --- a/pkg/chart/v2/loader/load.go +++ b/pkg/chart/v2/loader/load.go @@ -153,7 +153,7 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) { } fname := strings.TrimPrefix(f.Name, "charts/") - cname := strings.SplitN(fname, "/", 2)[0] + cname, _, _ := strings.Cut(fname, "/") 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, ModTime: f.ModTime, Data: f.Data}) diff --git a/pkg/chart/v2/util/dependencies.go b/pkg/chart/v2/util/dependencies.go index 019cd1cda..ae5b224ce 100644 --- a/pkg/chart/v2/util/dependencies.go +++ b/pkg/chart/v2/util/dependencies.go @@ -46,7 +46,6 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, if c != "" { // retrieve value vv, err := cvals.PathValue(cpath + c) - var errNoValue common.ErrNoValue if err == nil { // if not bool, warn if bv, ok := vv.(bool); ok { @@ -54,7 +53,7 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals common.Values, break } slog.Warn("returned non-bool value", "path", c, "chart", r.Name) - } else if !errors.As(err, &errNoValue) { + } else if _, ok := errors.AsType[common.ErrNoValue](err); !ok { // this is a real error slog.Warn("the method PathValue returned error", slog.Any("error", err)) } diff --git a/pkg/chart/v2/util/doc.go b/pkg/chart/v2/util/doc.go index ed741a83d..70dc7199f 100644 --- a/pkg/chart/v2/util/doc.go +++ b/pkg/chart/v2/util/doc.go @@ -42,4 +42,4 @@ into a Chart. When creating charts in memory, use the 'helm.sh/helm/pkg/chart' package directly. */ -package util // import "helm.sh/helm/v4/pkg/chart/v2/util" +package util diff --git a/pkg/chart/v2/util/save_test.go b/pkg/chart/v2/util/save_test.go index 1795226bc..6599addb2 100644 --- a/pkg/chart/v2/util/save_test.go +++ b/pkg/chart/v2/util/save_test.go @@ -298,13 +298,10 @@ func TestSaveDir(t *testing.T) { } 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", @@ -325,7 +322,6 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "63358874b93ea095c857cd66bcf5d0a4464840cf84a07547db744d81d6c5af59", }, { name: "Package 2 files", @@ -347,20 +343,26 @@ func TestRepeatableSave(t *testing.T) { Schema: []byte("{\n \"title\": \"Values\"\n}"), SchemaModTime: modTime, }, - want: "c2a43990053da788ad4e260d3b00d52a0b103ccc67ab9f48278a7b6dcfb2a4bd", }, } 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) + // Package the same chart twice, into separate directories so the + // second Save does not simply overwrite the first. + first, err := Save(test.chart, t.TempDir()) + require.NoError(t, err, "Failed to save") + second, err := Save(test.chart, t.TempDir()) require.NoError(t, err, "Failed to save") - // get shasum for package - result, err := sha256Sum(where) + + firstSum, err := sha256Sum(first) require.NoError(t, err, "Failed to check shasum") - // assert that the package SHA is what we wanted. - assert.Equal(t, test.want, result, "FormatName() result = %v, want %v", result, test.want) + secondSum, err := sha256Sum(second) + require.NoError(t, err, "Failed to check shasum") + + // Packaging a chart must be reproducible. The digest itself is not + // pinned here because the bytes depend on the compression + // implementation of the Go release Helm is built with. + assert.Equal(t, firstSum, secondSum, "Save() is not repeatable") }) } } diff --git a/pkg/cmd/dependency_build.go b/pkg/cmd/dependency_build.go index 2fb997e4c..89bcd8a1d 100644 --- a/pkg/cmd/dependency_build.go +++ b/pkg/cmd/dependency_build.go @@ -83,8 +83,7 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command { man.Verify = downloader.VerifyIfPossible } err = man.Build() - var e downloader.ErrRepoNotFound - if errors.As(err, &e) { + if e, ok := errors.AsType[downloader.ErrRepoNotFound](err); ok { return fmt.Errorf("%s. Please add the missing repos via 'helm repo add'", e.Error()) } return err diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 67b1cf47b..00bb4be9d 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -482,8 +482,7 @@ func reformatExecErrorMsg(filename string, err error) error { // If it can parse out details from that error message such as the line number, template it failed on, // and error description, then it will construct a new error that displays these details in a structured way. // If there are issues with parsing the error message, the err passed into the function should return instead. - var execError template.ExecError - if !errors.As(err, &execError) { + if _, ok := errors.AsType[template.ExecError](err); !ok { return err } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index aebb6f11c..e9432b195 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -447,7 +447,7 @@ func verifyInsecureSkipVerify(t *testing.T, g *HTTPGetter, caseName string, expe require.NoError(t, err) require.NotNil(t, returnVal, "Expected non nil value for http client") - transport := (returnVal.Transport).(*http.Transport) + transport := returnVal.Transport.(*http.Transport) gotValue := false if transport.TLSClientConfig != nil { gotValue = transport.TLSClientConfig.InsecureSkipVerify @@ -465,7 +465,7 @@ func TestDefaultHTTPTransportReuse(t *testing.T) { require.NotNil(t, httpClient1, "Expected non nil value for http client") - transport1 := (httpClient1.Transport).(*http.Transport) + transport1 := httpClient1.Transport.(*http.Transport) httpClient2, err := g.httpClient(g.opts) @@ -473,7 +473,7 @@ func TestDefaultHTTPTransportReuse(t *testing.T) { require.NotNil(t, httpClient2, "Expected non nil value for http client") - transport2 := (httpClient2.Transport).(*http.Transport) + transport2 := httpClient2.Transport.(*http.Transport) require.Equal(t, transport2, transport1, "Expected default transport to be reused") } @@ -489,7 +489,7 @@ func TestHTTPTransportOption(t *testing.T) { require.NotNil(t, httpClient1, "Expected non nil value for http client") - transport1 := (httpClient1.Transport).(*http.Transport) + transport1 := httpClient1.Transport.(*http.Transport) require.Equal(t, transport, transport1, "Expected transport option to be applied") @@ -499,7 +499,7 @@ func TestHTTPTransportOption(t *testing.T) { require.NotNil(t, httpClient2, "Expected non nil value for http client") - transport2 := (httpClient2.Transport).(*http.Transport) + transport2 := httpClient2.Transport.(*http.Transport) require.Equal(t, transport2, transport1, "Expected applied transport to be reused") diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 86574a3e5..60f6b6fa1 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -79,7 +79,6 @@ func newPodWithStatus(name string, status v1.PodStatus, namespace string) v1.Pod ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: ns, - SelfLink: "/api/v1/namespaces/default/pods/" + name, }, Spec: v1.PodSpec{ Containers: []v1.Container{{ diff --git a/pkg/registry/client.go b/pkg/registry/client.go index 4c7b9a6c8..672f5603a 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -231,7 +231,7 @@ type ( // Returns true if the host contains a path component (i.e., contains a '/'). func warnIfHostHasPath(host string) bool { if strings.Contains(host, "/") { - registryHost := strings.Split(host, "/")[0] + registryHost, _, _ := strings.Cut(host, "/") slog.Warn("registry login currently only supports registry hostname, not a repository path", "host", host, "suggested", registryHost) return true } @@ -322,6 +322,11 @@ func ensureTLSConfig(client *auth.Client, setConfig *tls.Config) (*tls.Config, e transport.TLSClientConfig = &tls.Config{} } + // Idle connections were established under the previous TLS configuration. + // Drop them so the settings being applied here take effect on the next + // request instead of being bypassed by a pooled connection. + transport.CloseIdleConnections() + return transport.TLSClientConfig, nil } diff --git a/pkg/repo/v1/repotest/server.go b/pkg/repo/v1/repotest/server.go index 06cdc9172..9eb32c47b 100644 --- a/pkg/repo/v1/repotest/server.go +++ b/pkg/repo/v1/repotest/server.go @@ -216,6 +216,19 @@ func (srv *OCIServer) RunWithReturn(t *testing.T, opts ...OCIServerOpt) *OCIServ go srv.ListenAndServe() + // NewOCIServer released the port it reserved so the registry could claim + // it, and ListenAndServe binds asynchronously. Wait for the port to accept + // connections so the login below does not race the listener coming up. + dialer := &net.Dialer{Timeout: time.Second} + require.Eventually(t, func() bool { + conn, err := dialer.DialContext(t.Context(), "tcp", srv.RegistryURL) + if err != nil { + return false + } + conn.Close() + return true + }, 30*time.Second, 20*time.Millisecond, "test registry never started listening on %s", srv.RegistryURL) + credentialsFile := filepath.Join(srv.Dir, "config.json") // init test client