Merge branch 'helm:main' into feature/enhance-dry-run-for-helm-4

pull/31616/head
MrJack 1 week ago committed by GitHub
commit 81b99295be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -48,7 +48,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # pinv4.37.8 uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # pinv4.37.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
@ -59,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # pinv4.37.8 uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # pinv4.37.9
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -73,4 +73,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # pinv4.37.8 uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # pinv4.37.9

@ -64,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional). # Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard # Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning" - name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
with: with:
sarif_file: results.sarif sarif_file: results.sarif

@ -13,7 +13,8 @@ formatters:
gofumpt: gofumpt:
module-path: helm.sh/helm/v4 module-path: helm.sh/helm/v4
extra-rules: false extra:
group-params: true
goimports: goimports:
local-prefixes: local-prefixes:
@ -95,7 +96,6 @@ linters:
- exposedSyncMutex - exposedSyncMutex
- hugeParam - hugeParam
- importShadow - importShadow
- paramTypeCombine
- ptrToRefParam - ptrToRefParam
- rangeValCopy - rangeValCopy
- todoCommentWithoutDetail - todoCommentWithoutDetail
@ -119,6 +119,7 @@ linters:
enable-default-rules: true enable-default-rules: true
max-open-files: 2048 max-open-files: 2048
rules: rules:
- name: bare-return
- name: early-return - name: early-return
arguments: arguments:
- "preserve-scope" - "preserve-scope"

@ -19,8 +19,8 @@ require (
github.com/fatih/color v1.19.0 github.com/fatih/color v1.19.0
github.com/fluxcd/cli-utils v1.2.2 github.com/fluxcd/cli-utils v1.2.2
github.com/foxcpp/go-mockdns v1.2.0 github.com/foxcpp/go-mockdns v1.2.0
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v1.0.0
github.com/gofrs/flock v0.13.0 github.com/gofrs/flock v0.13.1
github.com/gosuri/uitable v0.0.4 github.com/gosuri/uitable v0.0.4
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3

@ -128,10 +128,10 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=

@ -141,7 +141,7 @@ func (ch *Chart) AppVersion() string {
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
// //
// Deprecated: use CRDObjects() // Deprecated: Use CRDObjects instead.
func (ch *Chart) CRDs() []*common.File { func (ch *Chart) CRDs() []*common.File {
files := []*common.File{} files := []*common.File{}
// Find all resources in the crds/ directory // Find all resources in the crds/ directory

@ -43,6 +43,10 @@ func (l DirLoader) Load() (*chart.Chart, error) {
// //
// This loads charts only from directories. // This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) { func LoadDir(dir string) (*chart.Chart, error) {
return loadDir(dir, archive.MaxDecompressedChartSize)
}
func loadDir(dir string, budget int64) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir) topdir, err := filepath.Abs(dir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -64,6 +68,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files := []*archive.BufferedFile{} files := []*archive.BufferedFile{}
topdir += string(filepath.Separator) topdir += string(filepath.Separator)
budgetReader := archive.NewBudgetedReader(budget)
walk := func(name string, fi os.FileInfo, err error) error { walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir) n := strings.TrimPrefix(name, topdir)
@ -100,11 +105,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
} }
if fi.Size() > archive.MaxDecompressedFileSize { data, err := budgetReader.ReadFileWithBudget(name, fi.Size())
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
if err != nil { if err != nil {
return fmt.Errorf("error reading %s: %w", n, err) return fmt.Errorf("error reading %s: %w", n, err)
} }

@ -50,6 +50,16 @@ func TestLoadDir(t *testing.T) {
verifyDependenciesLock(t, c) verifyDependenciesLock(t, c)
} }
func TestLoadDirExceedsBudget(t *testing.T) {
_, err := loadDir("testdata/frobnitz", 1)
if err == nil {
t.Fatal("expected error when chart directory exceeds budget")
}
if !strings.Contains(err.Error(), "chart exceeds maximum decompressed size") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadDirWithDevNull(t *testing.T) { func TestLoadDirWithDevNull(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("test only works on unix systems with /dev/null present") t.Skip("test only works on unix systems with /dev/null present")

@ -18,7 +18,7 @@ package util
import ( import (
"fmt" "fmt"
"io" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -642,12 +642,6 @@ spec:
restartPolicy: Never restartPolicy: Never
` `
// Stderr is an io.Writer to which error messages can be written
//
// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward
// compatibility.
var Stderr io.Writer = os.Stderr
// CreateFrom creates a new chart, but scaffolds it from the src chart. // CreateFrom creates a new chart, but scaffolds it from the src chart.
func CreateFrom(chartfile *chart.Metadata, dest, src string) error { func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
schart, err := loader.Load(src) schart, err := loader.Load(src)
@ -795,8 +789,7 @@ func Create(name, dir string) (string, error) {
for _, file := range files { for _, file := range files {
if _, err := os.Stat(file.path); err == nil { if _, err := os.Stat(file.path); err == nil {
// There is no handle to a preferred output stream here. slog.Warn("Overwriting existing file", slog.String("file", file.path))
fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path)
} }
if err := writeFile(file.path, file.content); err != nil { if err := writeFile(file.path, file.content); err != nil {
return cdir, err return cdir, err

@ -18,6 +18,7 @@ package util
import ( import (
"bytes" "bytes"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -98,7 +99,11 @@ func TestCreateFrom(t *testing.T) {
func TestCreate_Overwrite(t *testing.T) { func TestCreate_Overwrite(t *testing.T) {
tdir := t.TempDir() tdir := t.TempDir()
var errlog bytes.Buffer prev := slog.Default()
t.Cleanup(func() { slog.SetDefault(prev) })
var logBuf bytes.Buffer
slog.SetDefault(slog.New(slog.NewJSONHandler(&logBuf, nil)))
_, err := Create("foo", tdir) _, err := Create("foo", tdir)
require.NoError(t, err) require.NoError(t, err)
@ -109,7 +114,6 @@ func TestCreate_Overwrite(t *testing.T) {
writeFile(tplname, []byte("FOO")) writeFile(tplname, []byte("FOO"))
// Now re-run the create // Now re-run the create
Stderr = &errlog
_, err = Create("foo", tdir) _, err = Create("foo", tdir)
require.NoError(t, err) require.NoError(t, err)
@ -118,7 +122,7 @@ func TestCreate_Overwrite(t *testing.T) {
require.NotEqual(t, "FOO", string(data), "File that should have been modified was not.") require.NotEqual(t, "FOO", string(data), "File that should have been modified was not.")
assert.NotEqual(t, 0, errlog.Len(), "Expected warnings about overwriting files.") assert.NotEqual(t, 0, logBuf.Len(), "Expected warnings about overwriting files.")
} }
func TestValidateChartName(t *testing.T) { func TestValidateChartName(t *testing.T) {

@ -1,110 +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 util
import (
"errors"
"fmt"
"regexp"
)
// validName is a regular expression for resource names.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
var (
// errMissingName indicates that a release (name) was not provided.
errMissingName = errors.New("no name provided")
// errInvalidName indicates that an invalid release name was provided
errInvalidName = fmt.Errorf(
"invalid release name, must match regex %s and the length must not be longer than 53",
validName.String())
// errInvalidKubernetesName indicates that the name does not meet the Kubernetes
// restrictions on metadata names.
errInvalidKubernetesName = fmt.Errorf(
"invalid metadata name, must match regex %s and the length must not be longer than 253",
validName.String())
)
const (
// According to the Kubernetes docs (https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#rfc-1035-label-names)
// some resource names have a max length of 63 characters while others have a max
// length of 253 characters. As we cannot be sure the resources used in a chart, we
// therefore need to limit it to 63 chars and reserve 10 chars for additional part to name
// of the resource. The reason is that chart maintainers can use release name as part of
// the resource name (and some additional chars).
maxReleaseNameLen = 53
// maxMetadataNameLen is the maximum length Kubernetes allows for any name.
maxMetadataNameLen = 253
)
// ValidateReleaseName performs checks for an entry for a Helm release name
//
// For Helm to allow a name, it must be below a certain character count (53) and also match
// a regular expression.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
func ValidateReleaseName(name string) error {
// This case is preserved for backwards compatibility
if name == "" {
return errMissingName
}
if len(name) > maxReleaseNameLen || !validName.MatchString(name) {
return errInvalidName
}
return nil
}
// ValidateMetadataName validates the name field of a Kubernetes metadata object.
//
// Empty strings, strings longer than 253 chars, or strings that don't match the regexp
// will fail.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// Deprecated: remove in Helm 4. Name validation now uses rules defined in
// pkg/lint/rules.validateMetadataNameFunc()
func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName
}
return nil
}

@ -1,93 +0,0 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package util
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestValidateReleaseName is a regression test for ValidateName
//
// Kubernetes has strict naming conventions for resource names. This test represents
// those conventions.
//
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// NOTE: At the time of this writing, the docs above say that names cannot begin with
// digits. However, `kubectl`'s regular expression explicit allows this, and
// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
func TestValidateReleaseName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
err := ValidateReleaseName(input)
if expectPass {
assert.NoErrorf(t, err, "Expected release name %q to pass validation", input)
} else {
assert.Errorf(t, err, "Expected release name %q to fail validation, but it passed", input)
}
}
}
func TestValidateMetadataName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": true,
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
err := ValidateMetadataName(input)
if expectPass {
assert.NoError(t, err, "Expected %q to succeed", input)
} else {
assert.Error(t, err, "Expected %q to fail", input)
}
}
}

@ -101,11 +101,15 @@ type ChartVersion struct {
// Search performs a search against the monocular search API // Search performs a search against the monocular search API
// //
// Deprecated: Use SearchWithContext instead. // Deprecated: Use SearchWithContext instead.
//
//go:fix inline
func (c *Client) Search(term string) ([]SearchResult, error) { func (c *Client) Search(term string) ([]SearchResult, error) {
return c.SearchWithContext(context.Background(), term) return c.SearchWithContext(context.Background(), term)
} }
// SearchWithContext performs a search against the monocular search API // SearchWithContext performs a search against the monocular search API
//
// TODO Helm v5: Rename this to Search (remove the current deprecated Search method) and 'go:fix inline' SearchWithContext method to call Search.
func (c *Client) SearchWithContext(ctx context.Context, term string) ([]SearchResult, error) { func (c *Client) SearchWithContext(ctx context.Context, term string) ([]SearchResult, error) {
// Create the URL to the search endpoint // Create the URL to the search endpoint
// Note, this is currently an internal API for the Hub. This should be // Note, this is currently an internal API for the Hub. This should be

@ -48,7 +48,7 @@ var Extractors = map[string]Extractor{
// Convert a media type to an extractor extension. // Convert a media type to an extractor extension.
// //
// This should be refactored in Helm 4, combined with the extension-based mechanism. // TODO Helm v5: refactor this, combined with the extension-based mechanism.
func mediaTypeToExtension(mt string) (string, bool) { func mediaTypeToExtension(mt string) (string, bool) {
switch strings.ToLower(mt) { switch strings.ToLower(mt) {
case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar": case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar":

@ -153,7 +153,7 @@ func NewForSource(source, version string) (installer Installer, err error) {
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 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 return installer, err
} }
// FindSource determines the correct Installer for the given source. // FindSource determines the correct Installer for the given source.

@ -53,7 +53,7 @@ func detectPluginRoot(extractDir string) (string, error) {
// validatePluginName checks if the plugin directory name matches the plugin name // validatePluginName checks if the plugin directory name matches the plugin name
// from plugin.yaml when the plugin is in a subdirectory. // from plugin.yaml when the plugin is in a subdirectory.
func validatePluginName(pluginRoot string, expectedName string) error { func validatePluginName(pluginRoot, expectedName string) error {
// Only validate if plugin is in a subdirectory // Only validate if plugin is in a subdirectory
dirName := filepath.Base(pluginRoot) dirName := filepath.Base(pluginRoot)
if dirName == expectedName { if dirName == expectedName {

@ -212,7 +212,7 @@ func (p *ExtismV1PluginRuntime) Invoke(ctx context.Context, input *Input) (*Outp
return output, nil return output, nil
} }
func buildManifest(pluginDir string, tmpDir string, rc *RuntimeConfigExtismV1) (extism.Manifest, error) { func buildManifest(pluginDir, tmpDir string, rc *RuntimeConfigExtismV1) (extism.Manifest, error) {
wasmFile := filepath.Join(pluginDir, ExtismV1WasmBinaryFilename) wasmFile := filepath.Join(pluginDir, ExtismV1WasmBinaryFilename)
allowedHosts := rc.AllowedHosts allowedHosts := rc.AllowedHosts

@ -47,7 +47,7 @@ type RuntimeConfigSubprocess struct {
PlatformHooks PlatformHooks `yaml:"platformHooks"` PlatformHooks PlatformHooks `yaml:"platformHooks"`
// ProtocolCommands allows the plugin to specify protocol specific commands // ProtocolCommands allows the plugin to specify protocol specific commands
// //
// Obsolete/deprecated: This is a compatibility hangover from the old plugin downloader mechanism, which was extended // Deprecated: This is an obsolete compatibility hangover from the old plugin downloader mechanism, which was extended
// to support multiple protocols in a given plugin. The command supplied in PlatformCommand should implement protocol // to support multiple protocols in a given plugin. The command supplied in PlatformCommand should implement protocol
// specific logic by inspecting the download URL // specific logic by inspecting the download URL
ProtocolCommands []SubprocessProtocolCommand `yaml:"protocolCommands,omitempty"` ProtocolCommands []SubprocessProtocolCommand `yaml:"protocolCommands,omitempty"`
@ -112,7 +112,7 @@ func (r *SubprocessPluginRuntime) Invoke(ctx context.Context, input *Input) (*Ou
// InvokeWithEnv executes a plugin command with custom environment and I/O streams // InvokeWithEnv executes a plugin command with custom environment and I/O streams
// This method allows execution with different command/args than the plugin's default // This method allows execution with different command/args than the plugin's default
func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv []string, env []string, stdin io.Reader, stdout, stderr io.Writer) error { func (r *SubprocessPluginRuntime) InvokeWithEnv(main string, argv, env []string, stdin io.Reader, stdout, stderr io.Writer) error {
mainCmdExp := os.ExpandEnv(main) mainCmdExp := os.ExpandEnv(main)
cmd := exec.CommandContext(context.Background(), mainCmdExp, argv...) cmd := exec.CommandContext(context.Background(), mainCmdExp, argv...)
cmd.Env = slices.Clone(os.Environ()) cmd.Env = slices.Clone(os.Environ())

@ -111,7 +111,7 @@ func GetPluginSigningInfo(metadata Metadata) (*SigningInfo, error) {
}, nil }, nil
} }
func validateProvenanceHash(blockContent string, tarballPath string) bool { func validateProvenanceHash(blockContent, tarballPath string) bool {
// Parse provenance to get the expected hash // Parse provenance to get the expected hash
_, sums, err := parsePluginMessageBlock([]byte(blockContent)) _, sums, err := parsePluginMessageBlock([]byte(blockContent))
if err != nil { if err != nil {

@ -41,7 +41,7 @@ func (fn FilterFunc) Filter(rels []*v2.Release) (rets []*v2.Release) {
rets = append(rets, rel) rets = append(rets, rel)
} }
} }
return return rets
} }
// Any returns a FilterFunc that filters a list of releases // Any returns a FilterFunc that filters a list of releases

@ -137,7 +137,7 @@ func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.H
return h return h
} }
func lessByKind(_ any, _ any, kindA string, kindB string, o KindSortOrder) bool { func lessByKind(_, _ any, kindA, kindB string, o KindSortOrder) bool {
ordering := make(map[string]int, len(o)) ordering := make(map[string]int, len(o))
for v, k := range o { for v, k := range o {
ordering[k] = v ordering[k] = v

@ -49,7 +49,7 @@ func AssertGoldenString(t TestingT, actual, filename string) {
} }
// AssertGoldenFile asserts that the content of the actual file matches the contents of the expected file // AssertGoldenFile asserts that the content of the actual file matches the contents of the expected file
func AssertGoldenFile(t TestingT, actualFileName string, expectedFilename string) { func AssertGoldenFile(t TestingT, actualFileName, expectedFilename string) {
t.Helper() t.Helper()
actual, err := os.ReadFile(actualFileName) actual, err := os.ReadFile(actualFileName)

@ -34,7 +34,7 @@ var (
// //
// Increment major number for new feature additions and behavioral changes. // Increment major number for new feature additions and behavioral changes.
// Increment minor number for bug fixes and performance enhancements. // Increment minor number for bug fixes and performance enhancements.
version = "v4.2" version = "v4.3"
// metadata is extra build time data // metadata is extra build time data
metadata = "" metadata = ""

@ -508,8 +508,8 @@ func (cfg *Configuration) renderResources(ctx context.Context, ch *chart.Chart,
newDir = filepath.Join(outputDir, releaseName) newDir = filepath.Join(outputDir, releaseName)
} }
// NOTE: We do not have to worry about the post-renderer because // NOTE: We do not have to worry about the post-renderer because
// output dir is only used by `helm template`. In the next major // output dir is only used by `helm template`.
// release, we should move this logic to template only as it is not // TODO Helm v5: move this logic to template only as it is not
// used by install or upgrade // used by install or upgrade
err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name]) err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
if err != nil { if err != nil {

@ -77,12 +77,14 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*")
// If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs. // If a chart is unpacked, this will check the unpacked chart's `charts/` directory for tarballs.
// Technically, this is COMPLETELY unnecessary, and should be removed in Helm 4. It is here //
// to preserved backward compatibility. In Helm 2/3, there is a "difference" between // Technically, this is COMPLETELY unnecessary. It is here to preserve backward
// compatibility. In Helm 2/3, there is a "difference" between
// the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outputs // the tgz version (which outputs "ok" if it unpacks) and the loaded version (which outputs
// "unpacked"). Early in Helm 2's history, this would have made a difference. But it no // "unpacked"). Early in Helm 2's history, this would have made a difference. But it no
// longer does. However, since this code shipped with Helm 3, the output must remain stable // longer does. However, since this code shipped with Helm 3, the output must remain stable.
// until Helm 4. //
// TODO Helm v5: remove this.
switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); { switch archives, err := filepath.Glob(filepath.Join(chartpath, "charts", filename)); {
case err != nil: case err != nil:
return "bad pattern" return "bad pattern"
@ -156,7 +158,9 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
// stat an archive and return a message if the stat is successful // stat an archive and return a message if the stat is successful
// //
// This is a refactor of the code originally in dependencyStatus. It is here to // This is a refactor of the code originally in dependencyStatus. It is here to
// support legacy behavior, and should be removed in Helm 4. // support legacy behavior.
//
// TODO Helm v5: remove this.
func statArchiveForStatus(archive string, dep *chart.Dependency) string { func statArchiveForStatus(archive string, dep *chart.Dependency) string {
if _, err := os.Stat(archive); err == nil { if _, err := os.Stat(archive); err == nil {
c, err := loader.Load(archive) c, err := loader.Load(archive)

@ -506,7 +506,7 @@ func (i *Install) RunWithContext(ctx context.Context, ch ci.Charter, vals map[st
return rel, err return rel, err
} }
func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) { func (i *Install) performInstallCtx(ctx context.Context, rel *release.Release, toBeAdopted, resources kube.ResourceList) (*release.Release, error) {
type Msg struct { type Msg struct {
r *release.Release r *release.Release
e error e error
@ -533,7 +533,7 @@ func (i *Install) getGoroutineCount() int32 {
return i.goroutineCount.Load() return i.goroutineCount.Load()
} }
func (i *Install) performInstall(rel *release.Release, toBeAdopted kube.ResourceList, resources kube.ResourceList) (*release.Release, error) { func (i *Install) performInstall(rel *release.Release, toBeAdopted, resources kube.ResourceList) (*release.Release, error) {
var err error var err error
// pre-install hooks // pre-install hooks
if !i.DisableHooks { if !i.DisableHooks {
@ -747,7 +747,7 @@ func (i *Install) replaceRelease(rel *release.Release) error {
} }
// write the <data> to <output-dir>/<name>. <appendData> controls if the file is created or content will be appended // write the <data> to <output-dir>/<name>. <appendData> controls if the file is created or content will be appended
func writeToFile(outputDir string, name string, data string, appendData bool) error { func writeToFile(outputDir, name, data string, appendData bool) error {
outfileName := outputDir + string(filepath.Separator) + name outfileName := outputDir + string(filepath.Separator) + name
err := ensureDirectoryForFile(outfileName) err := ensureDirectoryForFile(outfileName)

@ -90,7 +90,7 @@ func NewPushWithOpts(opts ...PushOpt) *Push {
} }
// Run executes 'helm push' against the given chart archive. // Run executes 'helm push' against the given chart archive.
func (p *Push) Run(chartRef string, remote string) (string, error) { func (p *Push) Run(chartRef, remote string) (string, error) {
var out strings.Builder var out strings.Builder
c := uploader.ChartUploader{ c := uploader.ChartUploader{

@ -82,7 +82,7 @@ func NewRegistryLogin(cfg *Configuration) *RegistryLogin {
} }
// Run executes the registry login operation // Run executes the registry login operation
func (a *RegistryLogin) Run(_ io.Writer, hostname string, username string, password string, opts ...RegistryLoginOpt) error { func (a *RegistryLogin) Run(_ io.Writer, hostname, username, password string, opts ...RegistryLoginOpt) error {
for _, opt := range opts { for _, opt := range opts {
if err := opt(a); err != nil { if err := opt(a); err != nil {
return err return err

@ -471,7 +471,7 @@ func isReleaseApplyMethodClientSideApply(applyMethod string) bool {
return applyMethod == "" || applyMethod == string(release.ApplyMethodClientSideApply) return applyMethod == "" || applyMethod == string(release.ApplyMethodClientSideApply)
} }
func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current kube.ResourceList, target kube.ResourceList, originalRelease *release.Release, serverSideApply bool) { func (u *Upgrade) releasingUpgrade(c chan<- resultMessage, upgradedRelease *release.Release, current, target kube.ResourceList, originalRelease *release.Release, serverSideApply bool) {
// pre-upgrade hooks // pre-upgrade hooks
if !u.DisableHooks { if !u.DisableHooks {
@ -682,7 +682,7 @@ func mergeCustomLabels(current, desired map[string]string) map[string]string {
return labels return labels
} }
func getUpgradeServerSideValue(serverSideOption string, releaseApplyMethod string) (bool, error) { func getUpgradeServerSideValue(serverSideOption, releaseApplyMethod string) (bool, error) {
switch serverSideOption { switch serverSideOption {
case "auto": case "auto":
return releaseApplyMethod == "ssa", nil return releaseApplyMethod == "ssa", nil

@ -75,7 +75,7 @@ func newMissingDeployment(name, namespace string) *resource.Info {
return info return info
} }
func newDeploymentWithOwner(name, namespace string, labels map[string]string, annotations map[string]string) *resource.Info { func newDeploymentWithOwner(name, namespace string, labels, annotations map[string]string) *resource.Info {
obj := &appsv1.Deployment{ obj := &appsv1.Deployment{
ObjectMeta: v1.ObjectMeta{ ObjectMeta: v1.ObjectMeta{
Name: name, Name: name,

@ -87,7 +87,9 @@ func (kv *KubeVersion) String() string {
// GitVersion returns the full Kubernetes version string. // GitVersion returns the full Kubernetes version string.
// //
// Deprecated: use KubeVersion.Version. // Deprecated: Use KubeVersion.Version.
//
//go:fix inline
func (kv *KubeVersion) GitVersion() string { return kv.Version } func (kv *KubeVersion) GitVersion() string { return kv.Version }
// ParseKubeVersion parses kubernetes version from string // ParseKubeVersion parses kubernetes version from string

@ -46,7 +46,7 @@ func TestDefaultCapabilities(t *testing.T) {
assert.Equalf(t, "20", kv.Minor, "Expected default KubeVersion.Minor to be 20, got %q", kv.Minor) assert.Equalf(t, "20", kv.Minor, "Expected default KubeVersion.Minor to be 20, got %q", kv.Minor)
hv := caps.HelmVersion hv := caps.HelmVersion
assert.Equalf(t, "v4.2", hv.Version, "Expected default HelmVersion to be v4.2, got %q", hv.Version) assert.Equalf(t, "v4.3", hv.Version, "Expected default HelmVersion to be v4.3, got %q", hv.Version)
} }
func TestParseKubeVersion(t *testing.T) { func TestParseKubeVersion(t *testing.T) {

@ -37,8 +37,10 @@ import (
// The default value is 100 MiB. // The default value is 100 MiB.
var MaxDecompressedChartSize int64 = 100 * 1024 * 1024 // Default 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. // MaxDecompressedFileSize was the per-file size limit enforced during chart loading.
// The size of the file is the decompressed version of it when it is stored in an archive. // It is no longer used internally; aggregate chart size is enforced via MaxDecompressedChartSize.
//
// Deprecated: Retained for backward compatibility with external callers. Will be removed in Helm v5.
var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB var MaxDecompressedFileSize int64 = 5 * 1024 * 1024 // Default 5 MiB
var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`) var drivePathPattern = regexp.MustCompile(`^[a-zA-Z]:/`)
@ -128,10 +130,6 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) {
return nil, fmt.Errorf("decompressed chart is larger than the maximum size %d", MaxDecompressedChartSize) 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) limitedReader := io.LimitReader(tr, remainingSize)
bytesWritten, err := io.Copy(b, limitedReader) bytesWritten, err := io.Copy(b, limitedReader)

@ -0,0 +1,74 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package archive
import (
"fmt"
"io"
"math"
"os"
)
// BudgetedReader tracks cumulative file reads against a size limit.
type BudgetedReader struct {
max int64
remaining int64
}
// NewBudgetedReader creates a BudgetedReader with the given maximum total size.
// The remaining budget is initialized to the maximum.
func NewBudgetedReader(limit int64) *BudgetedReader {
return &BudgetedReader{
max: limit,
remaining: limit,
}
}
// ReadFileWithBudget reads a file and decrements the remaining budget by the bytes read.
// It returns an error if the total would exceed the configured maximum.
// The read is capped via io.LimitReader so a file that grows between stat
// and read cannot cause unbounded memory allocation.
func (r *BudgetedReader) ReadFileWithBudget(path string, size int64) ([]byte, error) {
if size > r.remaining {
return nil, fmt.Errorf("chart exceeds maximum decompressed size of %d bytes", r.max)
}
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
// Read at most r.remaining+1 bytes so we can detect over-budget without
// allocating unbounded memory if the file grew since stat.
// Clamp to avoid int64 overflow when r.remaining is near math.MaxInt64.
limit := r.remaining
if limit < math.MaxInt64 {
limit++
}
data, err := io.ReadAll(io.LimitReader(f, limit))
if err != nil {
return nil, err
}
if int64(len(data)) > r.remaining {
return nil, fmt.Errorf("chart exceeds maximum decompressed size of %d bytes", r.max)
}
r.remaining -= int64(len(data))
return data, nil
}

@ -0,0 +1,128 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package archive
import (
"fmt"
"os"
"path/filepath"
"testing"
)
func TestReadFileWithBudget(t *testing.T) {
dir := t.TempDir()
writeFile := func(t *testing.T, name string, size int) string {
t.Helper()
p := filepath.Join(dir, name)
if err := os.WriteFile(p, make([]byte, size), 0o644); err != nil {
t.Fatal(err)
}
return p
}
tcs := []struct {
name string
check func(t *testing.T)
}{
{
name: "reads file and decrements budget",
check: func(t *testing.T) {
t.Helper()
p := writeFile(t, "small.txt", 100)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
limit := int64(1000)
br := NewBudgetedReader(limit)
data, err := br.ReadFileWithBudget(p, fi.Size())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(data) != 100 {
t.Fatalf("expected 100 bytes, got %d", len(data))
}
if br.remaining != 900 {
t.Fatalf("expected remaining=900, got %d", br.remaining)
}
},
},
{
name: "rejects file exceeding budget",
check: func(t *testing.T) {
t.Helper()
p := writeFile(t, "big.txt", 500)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
limit := int64(100)
br := NewBudgetedReader(limit)
_, err = br.ReadFileWithBudget(p, fi.Size())
if err == nil {
t.Fatal("expected error for file exceeding budget")
}
expectedErr := fmt.Sprintf("chart exceeds maximum decompressed size of %d bytes", limit)
if err.Error() != expectedErr {
t.Fatalf("expected %q, got %q", expectedErr, err.Error())
}
if br.remaining != 100 {
t.Fatalf("budget should not change on rejection, got %d", br.remaining)
}
},
},
{
name: "tracks budget across multiple reads",
check: func(t *testing.T) {
t.Helper()
remaining := int64(250)
br := NewBudgetedReader(remaining)
for i := range 3 {
p := writeFile(t, fmt.Sprintf("f%d.txt", i), 80)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
if _, err := br.ReadFileWithBudget(p, fi.Size()); err != nil {
t.Fatalf("read %d: unexpected error: %v", i, err)
}
}
if br.remaining != 10 {
t.Fatalf("expected remaining=10, got %d", br.remaining)
}
p := writeFile(t, "over.txt", 20)
fi, err := os.Stat(p)
if err != nil {
t.Fatalf("failed to stat %s: %v", p, err)
}
_, err = br.ReadFileWithBudget(p, fi.Size())
if err == nil {
t.Fatal("expected error when cumulative reads exceed budget")
}
},
},
}
for _, tc := range tcs {
t.Run(tc.name, tc.check)
}
}

@ -144,7 +144,7 @@ func (ch *Chart) AppVersion() string {
// CRDs returns a list of File objects in the 'crds/' directory of a Helm chart. // CRDs returns a list of File objects in the 'crds/' directory of a Helm chart.
// //
// Deprecated: use CRDObjects() // Deprecated: Use CRDObjects instead.
func (ch *Chart) CRDs() []*common.File { func (ch *Chart) CRDs() []*common.File {
files := []*common.File{} files := []*common.File{}
// Find all resources in the crds/ directory // Find all resources in the crds/ directory

@ -43,6 +43,10 @@ func (l DirLoader) Load() (*chart.Chart, error) {
// //
// This loads charts only from directories. // This loads charts only from directories.
func LoadDir(dir string) (*chart.Chart, error) { func LoadDir(dir string) (*chart.Chart, error) {
return loadDir(dir, archive.MaxDecompressedChartSize)
}
func loadDir(dir string, budget int64) (*chart.Chart, error) {
topdir, err := filepath.Abs(dir) topdir, err := filepath.Abs(dir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -64,6 +68,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
files := []*archive.BufferedFile{} files := []*archive.BufferedFile{}
topdir += string(filepath.Separator) topdir += string(filepath.Separator)
budgetReader := archive.NewBudgetedReader(budget)
walk := func(name string, fi os.FileInfo, err error) error { walk := func(name string, fi os.FileInfo, err error) error {
n := strings.TrimPrefix(name, topdir) n := strings.TrimPrefix(name, topdir)
@ -100,11 +105,7 @@ func LoadDir(dir string) (*chart.Chart, error) {
return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name) return fmt.Errorf("cannot load irregular file %s as it has file mode type bits set", name)
} }
if fi.Size() > archive.MaxDecompressedFileSize { data, err := budgetReader.ReadFileWithBudget(name, fi.Size())
return fmt.Errorf("chart file %q is larger than the maximum file size %d", fi.Name(), archive.MaxDecompressedFileSize)
}
data, err := os.ReadFile(name)
if err != nil { if err != nil {
return fmt.Errorf("error reading %s: %w", n, err) return fmt.Errorf("error reading %s: %w", n, err)
} }

@ -113,8 +113,8 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
c.Schema = f.Data c.Schema = f.Data
c.SchemaModTime = f.ModTime c.SchemaModTime = f.ModTime
// Deprecated: requirements.yaml is deprecated use Chart.yaml. // requirements.yaml is deprecated; dependencies are declared in Chart.yaml since
// We will handle it for you because we are nice people // apiVersion v2. Handled here for backwards compatibility.
case f.Name == "requirements.yaml": case f.Name == "requirements.yaml":
if c.Metadata == nil { if c.Metadata == nil {
c.Metadata = new(chart.Metadata) c.Metadata = new(chart.Metadata)
@ -128,7 +128,8 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
if c.Metadata.APIVersion == chart.APIVersionV1 { if c.Metadata.APIVersion == chart.APIVersionV1 {
c.Files = append(c.Files, &common.File{Name: f.Name, ModTime: f.ModTime, 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. // requirements.lock is deprecated; use Chart.lock. Handled here for backwards
// compatibility.
case f.Name == "requirements.lock": case f.Name == "requirements.lock":
c.Lock = new(chart.Lock) c.Lock = new(chart.Lock)
if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil { if err := yaml.Unmarshal(f.Data, &c.Lock); err != nil {

@ -50,6 +50,16 @@ func TestLoadDir(t *testing.T) {
verifyDependenciesLock(t, c) verifyDependenciesLock(t, c)
} }
func TestLoadDirExceedsBudget(t *testing.T) {
_, err := loadDir("testdata/frobnitz", 1)
if err == nil {
t.Fatal("expected error when chart directory exceeds budget")
}
if !strings.Contains(err.Error(), "chart exceeds maximum decompressed size") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestLoadDirWithDevNull(t *testing.T) { func TestLoadDirWithDevNull(t *testing.T) {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
t.Skip("test only works on unix systems with /dev/null present") t.Skip("test only works on unix systems with /dev/null present")

@ -643,7 +643,7 @@ spec:
// Stderr is an io.Writer to which error messages can be written // Stderr is an io.Writer to which error messages can be written
// //
// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward // TODO Helm v5: replace this. It is needed in Helm 3 to preserve API backward
// compatibility. // compatibility.
var Stderr io.Writer = os.Stderr var Stderr io.Writer = os.Stderr

@ -183,7 +183,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error {
} }
// Save Chart.lock // Save Chart.lock
// TODO: remove the APIVersion check when APIVersionV1 is not used anymore // TODO Helm v5: remove the APIVersion check when APIVersionV1 is not used anymore
if c.Metadata.APIVersion == chart.APIVersionV2 { if c.Metadata.APIVersion == chart.APIVersionV2 {
if c.Lock != nil { if c.Lock != nil {
ldata, err := yaml.Marshal(c.Lock) ldata, err := yaml.Marshal(c.Lock)

@ -100,8 +100,11 @@ func ValidateReleaseName(name string) error {
// The Kubernetes documentation is here, though it is not entirely correct: // The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names // https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
// //
// Deprecated: remove in Helm 4. Name validation now uses rules defined in // Deprecated: ValidateMetadataName is no longer used by Helm. Metadata name
// pkg/lint/rules.validateMetadataNameFunc() // validation is now performed by the lint rule validateMetadataNameFunc (see
// pkg/chart/v2/lint/rules).
//
// TODO Helm v5: remove this function.
func ValidateMetadataName(name string) error { func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) { if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName return errInvalidKubernetesName

@ -219,7 +219,7 @@ func envCSV(name string) (ls []string) {
if trimmed != "" { if trimmed != "" {
ls = strings.Split(trimmed, ",") ls = strings.Split(trimmed, ",")
} }
return return ls
} }
func envColorMode() string { func envColorMode() string {

@ -85,7 +85,7 @@ func ParseFormat(s string) (out Format, err error) {
default: default:
out, err = "", ErrInvalidFormatType out, err = "", ErrInvalidFormatType
} }
return return out, err
} }
// Writer is an interface that any type can implement to write supported formats // Writer is an interface that any type can implement to write supported formats

@ -104,7 +104,7 @@ func (o *createOptions) run(out io.Writer) error {
if !gates.ChartV3.IsEnabled() { if !gates.ChartV3.IsEnabled() {
return gates.ChartV3.Error() return gates.ChartV3.Error()
} }
return o.createV3Chart(out) return o.createV3Chart()
default: default:
return fmt.Errorf("unsupported chart API version: %s (supported: v2, v3)", o.chartAPIVersion) return fmt.Errorf("unsupported chart API version: %s (supported: v2, v3)", o.chartAPIVersion)
} }
@ -136,7 +136,7 @@ func (o *createOptions) createV2Chart(out io.Writer) error {
return err return err
} }
func (o *createOptions) createV3Chart(out io.Writer) error { func (o *createOptions) createV3Chart() error {
chartname := filepath.Base(o.name) chartname := filepath.Base(o.name)
cfile := &chartv3.Metadata{ cfile := &chartv3.Metadata{
Name: chartname, Name: chartname,
@ -157,7 +157,6 @@ func (o *createOptions) createV3Chart(out io.Writer) error {
return chartutilv3.CreateFrom(cfile, filepath.Dir(o.name), lstarter) return chartutilv3.CreateFrom(cfile, filepath.Dir(o.name), lstarter)
} }
chartutilv3.Stderr = out
_, err := chartutilv3.Create(chartname, filepath.Dir(o.name)) _, err := chartutilv3.Create(chartname, filepath.Dir(o.name))
return err return err
} }

@ -268,7 +268,7 @@ func (p *postRendererArgsSlice) GetSlice() []string {
return p.options.args return p.options.args
} }
func compVersionFlag(chartRef string, _ string) ([]string, cobra.ShellCompDirective) { func compVersionFlag(chartRef, _ string) ([]string, cobra.ShellCompDirective) {
chartInfo := strings.Split(chartRef, "/") chartInfo := strings.Split(chartRef, "/")
if len(chartInfo) != 2 { if len(chartInfo) != 2 {
return nil, cobra.ShellCompDirectiveNoFileComp return nil, cobra.ShellCompDirectiveNoFileComp

@ -214,7 +214,7 @@ func addInstallFlags(cmd *cobra.Command, f *pflag.FlagSet, client *action.Instal
// For `helm template`, these notes flags are legacy, unused, and should not show in help, but // For `helm template`, these notes flags are legacy, unused, and should not show in help, but
// must remain accepted for backwards compatibility in Helm 4. Deprecate and hide them for now // must remain accepted for backwards compatibility in Helm 4. Deprecate and hide them for now
// TODO remove these from template command in Helm 5 // TODO Helm v5: remove these from template command
if cmd.Name() == "template" { if cmd.Name() == "template" {
if err := cmd.Flags().MarkDeprecated("hide-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil { if err := cmd.Flags().MarkDeprecated("hide-notes", "this flag has no effect for 'helm template' and will be removed in Helm 5"); err != nil {
log.Fatal(err) log.Fatal(err)

@ -155,7 +155,7 @@ type releaseListWriter struct {
noColor bool noColor bool
} }
func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders bool, noColor bool) *releaseListWriter { func newReleaseListWriter(releases []*release.Release, timeFormat string, noHeaders, noColor bool) *releaseListWriter {
// Initialize the array so no results returns an empty array instead of null // Initialize the array so no results returns an empty array instead of null
elements := make([]releaseElement, 0, len(releases)) elements := make([]releaseElement, 0, len(releases))
for _, r := range releases { for _, r := range releases {

@ -268,7 +268,7 @@ func runPullTests(t *testing.T, tests []struct {
wantErrorMsg string wantErrorMsg string
expectFile string expectFile string
expectDir bool expectDir bool
}, outdir string, additionalFlags string, }, outdir, additionalFlags string,
) { ) {
t.Helper() t.Helper()
for _, tt := range tests { for _, tt := range tests {

@ -91,7 +91,7 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
} }
// Adapted from https://github.com/oras-project/oras // Adapted from https://github.com/oras-project/oras
func getUsernamePassword(usernameOpt string, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) { func getUsernamePassword(usernameOpt, passwordOpt string, passwordFromStdinOpt bool) (string, string, error) {
var err error var err error
username := usernameOpt username := usernameOpt
password := passwordOpt password := passwordOpt

@ -0,0 +1,109 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"bytes"
"fmt"
"io"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"helm.sh/helm/v4/pkg/action"
"helm.sh/helm/v4/pkg/chart/common"
kubefake "helm.sh/helm/v4/pkg/kube/fake"
"helm.sh/helm/v4/pkg/repo/v1/repotest"
)
func TestTemplateOCIRegistryMessagesNotOnStdout(t *testing.T) {
defer resetEnv()()
stdout, stderr := runOCIChartCommand(t, func(ref, registryConfig, contentCache string) []string {
return []string{
"template", "release-name", ref,
"--version", "0.1.0",
"--plain-http",
"--registry-config", registryConfig,
"--content-cache", contentCache,
}
})
require.NotEmpty(t, stdout)
require.NotContains(t, stdout, "Pulled:")
require.NotContains(t, stdout, "Digest:")
require.Contains(t, stderr, "Pulled:")
require.Contains(t, stderr, "Digest:")
}
func TestShowOCIRegistryMessagesNotOnStdout(t *testing.T) {
defer resetEnv()()
stdout, stderr := runOCIChartCommand(t, func(ref, registryConfig, contentCache string) []string {
return []string{
"show", "chart", ref,
"--version", "0.1.0",
"--plain-http",
"--registry-config", registryConfig,
"--content-cache", contentCache,
}
})
require.NotEmpty(t, stdout)
require.Contains(t, stdout, "name: oci-dependent-chart")
require.NotContains(t, stdout, "Pulled:")
require.NotContains(t, stdout, "Digest:")
require.Contains(t, stderr, "Pulled:")
require.Contains(t, stderr, "Digest:")
}
func runOCIChartCommand(t *testing.T, argsFn func(ref, registryConfig, contentCache string) []string) (string, string) {
t.Helper()
srv := repotest.NewTempServer(
t,
repotest.WithChartSourceGlob("testdata/testcharts/*.tgz*"),
)
t.Cleanup(func() { srv.Stop() })
ociSrv, err := repotest.NewOCIServer(t, srv.Root())
require.NoError(t, err)
ociSrv.Run(t)
ref := fmt.Sprintf("oci://%s/u/ocitestuser/oci-dependent-chart", ociSrv.RegistryURL)
registryConfig := filepath.Join(srv.Root(), "config.json")
contentCache := t.TempDir()
args := argsFn(ref, registryConfig, contentCache)
stdout := &bytes.Buffer{}
stderr := &bytes.Buffer{}
actionConfig := &action.Configuration{
Releases: storageFixture(),
KubeClient: &kubefake.PrintingKubeClient{Out: io.Discard},
Capabilities: common.DefaultCapabilities,
}
root, err := newRootCmdWithConfig(actionConfig, stdout, args, SetupLogging)
require.NoError(t, err)
root.SetOut(stdout)
root.SetErr(stderr)
root.SetArgs(args)
require.NoError(t, root.Execute(), "stdout:\n%s\nstderr:\n%s", stdout.String(), stderr.String())
return stdout.String(), stderr.String()
}

@ -119,7 +119,7 @@ func TestRepoRemove(t *testing.T) {
} }
} }
func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, cacheChartsFile string) { func createCacheFiles(rootDir, repoName string) (cacheIndexFile, cacheChartsFile string) {
cacheIndexFile = filepath.Join(rootDir, helmpath.CacheIndexFile(repoName)) cacheIndexFile = filepath.Join(rootDir, helmpath.CacheIndexFile(repoName))
mf, _ := os.Create(cacheIndexFile) mf, _ := os.Create(cacheIndexFile)
mf.Close() mf.Close()
@ -131,7 +131,7 @@ func createCacheFiles(rootDir string, repoName string) (cacheIndexFile string, c
return cacheIndexFile, cacheChartsFile return cacheIndexFile, cacheChartsFile
} }
func testCacheFiles(t *testing.T, cacheIndexFile string, cacheChartsFile string, repoName string) { func testCacheFiles(t *testing.T, cacheIndexFile, cacheChartsFile, repoName string) {
t.Helper() t.Helper()
_, err := os.Stat(cacheIndexFile) _, err := os.Stat(cacheIndexFile)
require.Errorf(t, err, "Error cache index file was not removed for repository %s", repoName) require.Errorf(t, err, "Error cache index file was not removed for repository %s", repoName)

@ -268,7 +268,7 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) e
} }
// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'. // Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
func compListChartsOfRepo(repoName string, prefix string) []string { func compListChartsOfRepo(repoName, prefix string) []string {
var charts []string var charts []string
path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName)) path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))

@ -82,9 +82,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showAllDesc, Long: showAllDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc, ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowAll client.OutputFormat = action.ShowAll
err := addRegistryClient(out, client) err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil { if err != nil {
return err return err
} }
@ -103,9 +103,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showValuesDesc, Long: showValuesDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc, ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowValues client.OutputFormat = action.ShowValues
err := addRegistryClient(out, client) err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil { if err != nil {
return err return err
} }
@ -124,9 +124,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showChartDesc, Long: showChartDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc, ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowChart client.OutputFormat = action.ShowChart
err := addRegistryClient(out, client) err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil { if err != nil {
return err return err
} }
@ -145,9 +145,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: readmeChartDesc, Long: readmeChartDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc, ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowReadme client.OutputFormat = action.ShowReadme
err := addRegistryClient(out, client) err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil { if err != nil {
return err return err
} }
@ -166,9 +166,9 @@ func newShowCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
Long: showCRDsDesc, Long: showCRDsDesc,
Args: require.ExactArgs(1), Args: require.ExactArgs(1),
ValidArgsFunction: validArgsFunc, ValidArgsFunction: validArgsFunc,
RunE: func(_ *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
client.OutputFormat = action.ShowCRDs client.OutputFormat = action.ShowCRDs
err := addRegistryClient(out, client) err := addRegistryClient(cmd.ErrOrStderr(), client)
if err != nil { if err != nil {
return err return err
} }
@ -224,8 +224,8 @@ func runShow(args []string, client *action.Show) (string, error) {
return client.Run(cp) return client.Run(cp)
} }
func addRegistryClient(out io.Writer, client *action.Show) error { func addRegistryClient(registryOut io.Writer, client *action.Show) error {
registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(registryOut, client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) return fmt.Errorf("missing registry client: %w", err)

@ -85,7 +85,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.KubeVersion = parsedKubeVersion client.KubeVersion = parsedKubeVersion
} }
registryClient, err := newRegistryClient(out, client.CertFile, client.KeyFile, client.CaFile, registryClient, err := newRegistryClient(cmd.ErrOrStderr(), client.CertFile, client.KeyFile, client.CaFile,
client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password) client.InsecureSkipTLSVerify, client.PlainHTTP, client.Username, client.Password)
if err != nil { if err != nil {
return fmt.Errorf("missing registry client: %w", err) return fmt.Errorf("missing registry client: %w", err)
@ -238,7 +238,7 @@ func isTestHook(h *release.Hook) bool {
// bug introduced by #8156. As part of the todo to refactor renderResources // bug introduced by #8156. As part of the todo to refactor renderResources
// this duplicate code should be removed. It is added here so that the API // this duplicate code should be removed. It is added here so that the API
// surface area is as minimally impacted as possible in fixing the issue. // surface area is as minimally impacted as possible in fixing the issue.
func writeToFile(outputDir string, name string, data string, appendData bool) error { func writeToFile(outputDir, name, data string, appendData bool) error {
outfileName := outputDir + string(filepath.Separator) + name outfileName := outputDir + string(filepath.Separator) + name
err := ensureDirectoryForFile(outfileName) err := ensureDirectoryForFile(outfileName)

@ -1 +1 @@
Version: v4.2 Version: v4.3

@ -1 +1 @@
version.BuildInfo{Version:"v4.2", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"} version.BuildInfo{Version:"v4.3", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v1.20"}

@ -177,10 +177,11 @@ func (m *Manager) Update() error {
// For the repositories Helm is not configured to know about, ensure Helm // For the repositories Helm is not configured to know about, ensure Helm
// has some information about them and, when possible, the index files // has some information about them and, when possible, the index files
// locally. // locally.
// TODO(mattfarina): Repositories should be explicitly added by end users //
// rather than automatic. In Helm v4 require users to add repositories. They // TODO Helm v5: require users to add repositories explicitly rather than
// should have to add them in order to make sure they are aware of the // adding them automatically. They should have to add them in order to make
// repositories and opt-in to any locations, for security. // sure they are aware of the repositories and opt-in to any locations, for
// security.
repoNames, err = m.ensureMissingRepos(repoNames, req) repoNames, err = m.ensureMissingRepos(repoNames, req)
if err != nil { if err != nil {
return err return err
@ -733,22 +734,17 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
var entry repo.ChartVersions var entry repo.ChartVersions
entry, err = findEntryByName(name, cr) entry, err = findEntryByName(name, cr)
if err != nil { if err != nil {
// TODO: Where linting is skipped in this function we should // TODO: Consider refactoring this function to reduce the number of returned values while preserving behavior.
// refactor to remove naked returns while ensuring the same return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
// behavior
//nolint:nakedret
return
} }
var ve *repo.ChartVersion var ve *repo.ChartVersion
ve, err = findVersionedEntry(version, entry) ve, err = findVersionedEntry(version, entry)
if err != nil { if err != nil {
//nolint:nakedret return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
return
} }
url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0]) url, err = repo.ResolveReferenceURL(repoURL, ve.URLs[0])
if err != nil { if err != nil {
//nolint:nakedret return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
return
} }
username = cr.Config.Username username = cr.Config.Username
password = cr.Config.Password password = cr.Config.Password
@ -757,8 +753,8 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
caFile = cr.Config.CAFile caFile = cr.Config.CAFile
certFile = cr.Config.CertFile certFile = cr.Config.CertFile
keyFile = cr.Config.KeyFile keyFile = cr.Config.KeyFile
//nolint:nakedret
return return url, username, password, insecureSkipTLSVerify, passCredentialsAll, caFile, certFile, keyFile, err
} }
url, err = repo.FindChartInRepoURL(repoURL, name, m.Getters, repo.WithChartVersion(version), repo.WithClientTLS(certFile, keyFile, caFile)) url, err = repo.FindChartInRepoURL(repoURL, name, m.Getters, repo.WithChartVersion(version), repo.WithClientTLS(certFile, keyFile, caFile))
if err == nil { if err == nil {

@ -79,6 +79,8 @@ func New(config *rest.Config) Engine {
// bar chart during render time. // bar chart during render time.
// //
// Deprecated: Use RenderWithContext instead. // Deprecated: Use RenderWithContext instead.
//
//go:fix inline
func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) { func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string, error) {
return e.RenderWithContext(context.Background(), chrt, values) return e.RenderWithContext(context.Background(), chrt, values)
} }
@ -102,6 +104,8 @@ func (e Engine) Render(chrt ci.Charter, values common.Values) (map[string]string
// that section of the values will be passed into the "foo" chart. And if that // that section of the values will be passed into the "foo" chart. And if that
// section contains a value named "bar", that value will be passed on to the // section contains a value named "bar", that value will be passed on to the
// bar chart during render time. // bar chart during render time.
//
// TODO Helm v5: Rename this to Render (remove the current deprecated Render method) and 'go:fix inline' RenderWithContext method to call Render.
func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values common.Values) (map[string]string, error) { func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values common.Values) (map[string]string, error) {
tmap := allTemplates(chrt, values) tmap := allTemplates(chrt, values)
return e.render(ctx, tmap) return e.render(ctx, tmap)
@ -109,28 +113,38 @@ func (e Engine) RenderWithContext(ctx context.Context, chrt ci.Charter, values c
// Render takes a chart, optional values, and value overrides, and attempts to // Render takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. // render the Go templates using the default options.
//
// Deprecated: Instantiate an Engine and call RenderWithContext instead.
//
// TODO Helm v5: Replace with a NewEngine function.
//
//go:fix inline
func Render(chrt ci.Charter, values common.Values) (map[string]string, error) { func Render(chrt ci.Charter, values common.Values) (map[string]string, error) {
return new(Engine).Render(chrt, values) return new(Engine).RenderWithContext(context.Background(), chrt, values)
} }
// RenderWithClient takes a chart, optional values, and value overrides, and attempts to // RenderWithClient takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template // render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client. // functions that interact with the client.
//
// TODO Helm v5: Replace with a NewEngine function that accepts a rest.Config option
func RenderWithClient(chrt ci.Charter, values common.Values, config *rest.Config) (map[string]string, error) { func RenderWithClient(chrt ci.Charter, values common.Values, config *rest.Config) (map[string]string, error) {
var clientProvider ClientProvider = clientProviderFromConfig{config} var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{ return Engine{
clientProvider: &clientProvider, clientProvider: &clientProvider,
}.Render(chrt, values) }.RenderWithContext(context.Background(), chrt, values)
} }
// RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to // RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template // render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client. // functions that interact with the client.
// This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed. // This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed.
//
// TODO Helm v5: Replace with a NewEngine function that accepts a ClientProvider option
func RenderWithClientProvider(chrt ci.Charter, values common.Values, clientProvider ClientProvider) (map[string]string, error) { func RenderWithClientProvider(chrt ci.Charter, values common.Values, clientProvider ClientProvider) (map[string]string, error) {
return Engine{ return Engine{
clientProvider: &clientProvider, clientProvider: &clientProvider,
}.Render(chrt, values) }.RenderWithContext(context.Background(), chrt, values)
} }
// renderable is an object that can be rendered. // renderable is an object that can be rendered.
@ -413,7 +427,7 @@ func parseTemplateExecErrorString(s string) (TraceableError, bool) {
// Special case: "template: no template %q associated with template %q" // 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 // 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) { func parseTemplateNoTemplateError(s, remainder string) (TraceableError, bool) {
if strings.HasPrefix(remainder, "no template ") { if strings.HasPrefix(remainder, "no template ") {
return TraceableError{message: s}, true return TraceableError{message: s}, true
} }

@ -447,7 +447,7 @@ func durationWeeks(v any) float64 {
// //
// v and m accept the same forms as asDuration (e.g. "2h13m", "30s"). // v and m accept the same forms as asDuration (e.g. "2h13m", "30s").
// On error, it returns time.Duration(0). If m is invalid, it returns v. // On error, it returns time.Duration(0). If m is invalid, it returns v.
func durationRoundTo(v any, m any) time.Duration { func durationRoundTo(v, m any) time.Duration {
d, err := asDuration(v) d, err := asDuration(v)
if err != nil { if err != nil {
return 0 return 0
@ -463,7 +463,7 @@ func durationRoundTo(v any, m any) time.Duration {
// Returns a time.Duration. // Returns a time.Duration.
// //
// On error, it returns time.Duration(0). If m is invalid, it returns v. // On error, it returns time.Duration(0). If m is invalid, it returns v.
func durationTruncateTo(v any, m any) time.Duration { func durationTruncateTo(v, m any) time.Duration {
d, err := asDuration(v) d, err := asDuration(v)
if err != nil { if err != nil {
return 0 return 0

@ -30,7 +30,7 @@ import (
"k8s.io/client-go/rest" "k8s.io/client-go/rest"
) )
type lookupFunc = func(apiversion string, resource string, namespace string, name string) (map[string]any, error) type lookupFunc = func(apiversion, resource, namespace, name string) (map[string]any, error)
// NewLookupFunction returns a function for looking up objects in the cluster. // NewLookupFunction returns a function for looking up objects in the cluster.
// //
@ -55,7 +55,7 @@ func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic
} }
func newLookupFunction(ctx context.Context, clientProvider ClientProvider) lookupFunc { func newLookupFunction(ctx context.Context, clientProvider ClientProvider) lookupFunc {
return func(apiversion string, kind string, namespace string, name string) (map[string]any, error) { return func(apiversion, kind, namespace, name string) (map[string]any, error) {
var client dynamic.ResourceInterface var client dynamic.ResourceInterface
c, namespaced, err := clientProvider.GetClientFor(apiversion, kind) c, namespaced, err := clientProvider.GetClientFor(apiversion, kind)
if err != nil { if err != nil {
@ -105,7 +105,7 @@ func newLookupFunction(ctx context.Context, clientProvider ClientProvider) looku
} }
// getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced. // getDynamicClientOnKind returns a dynamic client on an Unstructured type. This client can be further namespaced.
func getDynamicClientOnKind(apiversion string, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) { func getDynamicClientOnKind(apiversion, kind string, config *rest.Config) (dynamic.NamespaceableResourceInterface, bool, error) {
gvk := schema.FromAPIVersionAndKind(apiversion, kind) gvk := schema.FromAPIVersionAndKind(apiversion, kind)
apiRes, err := getAPIResourceForGVK(gvk, config) apiRes, err := getAPIResourceForGVK(gvk, config)
if err != nil { if err != nil {

@ -1281,8 +1281,8 @@ func upgradeClientSideFieldManager(info *resource.Info, dryRun bool, fieldValida
return patched, err return patched, err
} }
// Patch reource using server-side apply // Patch resource using server-side apply
func patchResourceServerSide(target *resource.Info, dryRun bool, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error { func patchResourceServerSide(target *resource.Info, dryRun, forceConflicts bool, fieldValidationDirective FieldValidationDirective) error {
helper := resource.NewHelper( helper := resource.NewHelper(
target.Client, target.Client,
target.Mapping). target.Mapping).

@ -73,7 +73,7 @@ type Interface interface {
// by "\n---\n") // by "\n---\n")
// //
// Validates against OpenAPI schema if validate is true. // Validates against OpenAPI schema if validate is true.
// TODO Helm 4: Integrate into Build with an argument // TODO Helm v5: Integrate into Build with an argument
BuildTable(reader io.Reader, validate bool) (ResourceList, error) BuildTable(reader io.Reader, validate bool) (ResourceList, error)
} }
@ -103,7 +103,7 @@ type Waiter interface {
// InterfaceWaitOptions defines an interface that extends Interface with // InterfaceWaitOptions defines an interface that extends Interface with
// methods that accept wait options. // methods that accept wait options.
// //
// TODO Helm 5: Remove InterfaceWaitOptions and integrate its method(s) into the Interface. // TODO Helm v5: Remove InterfaceWaitOptions and integrate its method(s) into the Interface.
type InterfaceWaitOptions interface { type InterfaceWaitOptions interface {
// GetWaiter gets the Kube.Waiter with options. // GetWaiter gets the Kube.Waiter with options.
GetWaiterWithOptions(ws WaitStrategy, opts ...WaitOption) (Waiter, error) GetWaiterWithOptions(ws WaitStrategy, opts ...WaitOption) (Waiter, error)

@ -1444,7 +1444,7 @@ func newReplicationController(name string, generationInSync bool) *corev1.Replic
} }
} }
func newReplicaSet(name string, replicas int, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet { func newReplicaSet(name string, replicas, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet {
d := newDeployment(name, replicas, 0, 0, generationInSync) d := newDeployment(name, replicas, 0, 0, generationInSync)
return &appsv1.ReplicaSet{ return &appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -1503,7 +1503,7 @@ func newPersistentVolumeClaim(name string, phase corev1.PersistentVolumeClaimPha
} }
} }
func newJob(name string, backoffLimit int, completions *int32, succeeded int, failed int) *batchv1.Job { func newJob(name string, backoffLimit int, completions *int32, succeeded, failed int) *batchv1.Job {
return &batchv1.Job{ return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: name, Name: name,

@ -108,7 +108,7 @@ func generateChartOCIAnnotations(meta *chart.Metadata, creationTime string) map[
} }
// addToMap takes an existing map and adds an item if the value is not empty // addToMap takes an existing map and adds an item if the value is not empty
func addToMap(inputMap map[string]string, newKey string, newValue string) map[string]string { func addToMap(inputMap map[string]string, newKey, newValue string) map[string]string {
// Add item to map if its // Add item to map if its
if strings.TrimSpace(newValue) != "" { if strings.TrimSpace(newValue) != "" {
inputMap[newKey] = newValue inputMap[newKey] = newValue

@ -276,7 +276,7 @@ func (c *Client) Login(host string, options ...LoginOption) error {
} }
// LoginOptBasicAuth returns a function that sets the username/password settings on login // LoginOptBasicAuth returns a function that sets the username/password settings on login
func LoginOptBasicAuth(username string, password string) LoginOption { func LoginOptBasicAuth(username, password string) LoginOption {
return func(o *loginOperation) { return func(o *loginOperation) {
o.client.username = username o.client.username = username
o.client.password = password o.client.password = password

@ -49,7 +49,7 @@ type PluginPullResult struct {
} }
// PullPlugin downloads a plugin from an OCI registry using artifact type // PullPlugin downloads a plugin from an OCI registry using artifact type
func (c *Client) PullPlugin(ref string, pluginName string, options ...PluginPullOption) (*PluginPullResult, error) { func (c *Client) PullPlugin(ref, pluginName string, options ...PluginPullOption) (*PluginPullResult, error) {
operation := &pluginPullOperation{ operation := &pluginPullOperation{
pluginName: pluginName, pluginName: pluginName,
} }

@ -41,7 +41,7 @@ func (fn FilterFunc) Filter(rels []*rspb.Release) (rets []*rspb.Release) {
rets = append(rets, rel) rets = append(rets, rel)
} }
} }
return return rets
} }
// Any returns a FilterFunc that filters a list of releases // Any returns a FilterFunc that filters a list of releases

@ -137,7 +137,7 @@ func sortHooksByKind(hooks []*release.Hook, ordering KindSortOrder) []*release.H
return h return h
} }
func lessByKind(_ any, _ any, kindA string, kindB string, o KindSortOrder) bool { func lessByKind(_, _ any, kindA, kindB string, o KindSortOrder) bool {
ordering := make(map[string]int, len(o)) ordering := make(map[string]int, len(o))
for v, k := range o { for v, k := range o {
ordering[k] = v ordering[k] = v

@ -104,14 +104,15 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) {
} }
// Create the chart list file in the cache directory // Create the chart list file in the cache directory
var charts strings.Builder var charts bytes.Buffer
for name := range indexFile.Entries { for name := range indexFile.Entries {
fmt.Fprintln(&charts, name) charts.WriteString(name)
charts.WriteByte('\n') // Terminate each entry with a newline
} }
chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name)) chartsFile := filepath.Join(r.CachePath, helmpath.CacheChartsFile(r.Config.Name))
os.MkdirAll(filepath.Dir(chartsFile), 0o755) os.MkdirAll(filepath.Dir(chartsFile), 0o755)
fileutil.AtomicWriteFile(chartsFile, bytes.NewReader([]byte(charts.String())), 0o644) fileutil.AtomicWriteFile(chartsFile, &charts, 0o644)
// Create the index file in the cache directory // Create the index file in the cache directory
fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name)) fname := filepath.Join(r.CachePath, helmpath.CacheIndexFile(r.Config.Name))
@ -172,7 +173,7 @@ func WithInsecureSkipTLSVerify(insecureSkipTLSVerify bool) FindChartInRepoURLOpt
// FindChartInRepoURL finds chart in chart repository pointed by repoURL // FindChartInRepoURL finds chart in chart repository pointed by repoURL
// without adding repo to repositories // without adding repo to repositories
func FindChartInRepoURL(repoURL string, chartName string, getters getter.Providers, options ...FindChartInRepoURLOption) (string, error) { func FindChartInRepoURL(repoURL, chartName string, getters getter.Providers, options ...FindChartInRepoURLOption) (string, error) {
opts := findChartInRepoURLOptions{} opts := findChartInRepoURLOptions{}
for _, option := range options { for _, option := range options {
option(&opts) option(&opts)

@ -151,7 +151,7 @@ func (i IndexFile) MustAdd(md *chart.Metadata, filename, baseURL, digest string)
// Add adds a file to the index and logs an error. // Add adds a file to the index and logs an error.
// //
// Deprecated: Use index.MustAdd instead. // Deprecated: Use IndexFile.MustAdd instead. Add logs errors; MustAdd returns them.
func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) { func (i IndexFile) Add(md *chart.Metadata, filename, baseURL, digest string) {
if err := i.MustAdd(md, filename, baseURL, digest); err != nil { if err := i.MustAdd(md, filename, baseURL, digest); err != nil {
slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err) slog.Error("skipping loading invalid entry for chart", "name", md.Name, "version", md.Version, "file", filename, "error", err)
@ -290,21 +290,22 @@ type ChartVersion struct {
Removed bool `json:"removed,omitempty"` Removed bool `json:"removed,omitempty"`
Digest string `json:"digest,omitempty"` Digest string `json:"digest,omitempty"`
// ChecksumDeprecated is deprecated in Helm 3, and therefore ignored. Helm 3 replaced // Deprecated: ChecksumDeprecated is ignored (Helm 3 replaced it with Digest). It is
// this with Digest. However, with a strict YAML parser enabled, a field must be // retained only so a strict YAML parser accepts the "checksum" field for backwards
// present on the struct for backwards compatibility. // compatibility; do not use it.
ChecksumDeprecated string `json:"checksum,omitempty"` ChecksumDeprecated string `json:"checksum,omitempty"`
// EngineDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict // Deprecated: EngineDeprecated is ignored (removed in Helm 3). It is retained only so a
// YAML parser enabled, this field must be present. // strict YAML parser accepts the "engine" field for backwards compatibility; do not use it.
EngineDeprecated string `json:"engine,omitempty"` EngineDeprecated string `json:"engine,omitempty"`
// TillerVersionDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict // Deprecated: TillerVersionDeprecated is ignored (removed in Helm 3). It is retained only
// YAML parser enabled, this field must be present. // so a strict YAML parser accepts the "tillerVersion" field for backwards compatibility;
// do not use it.
TillerVersionDeprecated string `json:"tillerVersion,omitempty"` TillerVersionDeprecated string `json:"tillerVersion,omitempty"`
// URLDeprecated is deprecated in Helm 3, superseded by URLs. It is ignored. However, // Deprecated: URLDeprecated is ignored (superseded by URLs). It is retained only so a
// with a strict YAML parser enabled, this must be present on the struct. // strict YAML parser accepts the "url" field for backwards compatibility; do not use it.
URLDeprecated string `json:"url,omitempty"` URLDeprecated string `json:"url,omitempty"`
} }

@ -27,7 +27,7 @@ func (lbs labels) keys() (ls []string) {
for key := range lbs { for key := range lbs {
ls = append(ls, key) ls = append(ls, key)
} }
return return ls
} }
func (lbs labels) match(set labels) bool { func (lbs labels) match(set labels) bool {

@ -281,7 +281,7 @@ type SQLReleaseCustomLabelWrapper struct {
} }
// NewSQL initializes a new sql driver. // NewSQL initializes a new sql driver.
func NewSQL(connectionString string, namespace string) (*SQL, error) { func NewSQL(connectionString, namespace string) (*SQL, error) {
db, err := sqlx.Connect(postgreSQLDialect, connectionString) db, err := sqlx.Connect(postgreSQLDialect, connectionString)
if err != nil { if err != nil {
return nil, err return nil, err
@ -690,7 +690,7 @@ func (s *SQL) Delete(key string) (release.Releaser, error) {
} }
// Get release custom labels from database // Get release custom labels from database
func (s *SQL) getReleaseCustomLabels(key string, _ string) (map[string]string, error) { func (s *SQL) getReleaseCustomLabels(key, _ string) (map[string]string, error) {
query, args, err := s.statementBuilder. query, args, err := s.statementBuilder.
Select(sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableValueColumn). Select(sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableValueColumn).
From(sqlCustomLabelsTableName). From(sqlCustomLabelsTableName).

@ -493,7 +493,7 @@ func TestSqlDelete(t *testing.T) {
assert.Equalf(t, rel, deletedRelease, "Expected release {%v}, got {%v}", rel, deletedRelease) assert.Equalf(t, rel, deletedRelease, "Expected release {%v}, got {%v}", rel, deletedRelease)
} }
func mockGetReleaseCustomLabels(mock sqlmock.Sqlmock, key string, namespace string, labels map[string]string) { func mockGetReleaseCustomLabels(mock sqlmock.Sqlmock, key, namespace string, labels map[string]string) {
query := fmt.Sprintf( query := fmt.Sprintf(
regexp.QuoteMeta("SELECT %s, %s FROM %s WHERE %s = $1 AND %s = $2"), regexp.QuoteMeta("SELECT %s, %s FROM %s WHERE %s = $1 AND %s = $2"),
sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableKeyColumn,

Loading…
Cancel
Save