Merge remote-tracking branch 'upstream/main' into em/reinstate-logger-param

pull/31411/head
Evans Mungai 11 months ago
commit 0f90c83118
No known key found for this signature in database
GPG Key ID: BBEB812143DD14E1

@ -1,8 +1,8 @@
BINDIR := $(CURDIR)/bin BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64 TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
BINNAME ?= helm BINNAME ?= helm
GOBIN = $(shell go env GOBIN) GOBIN = $(shell go env GOBIN)
@ -69,6 +69,8 @@ LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_M
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER) LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMajor=$(K8S_MODULES_MAJOR_VER)
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMinor=$(K8S_MODULES_MINOR_VER)
.PHONY: all .PHONY: all
all: build all: build

@ -126,7 +126,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName)) linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files // We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { if !isYamlFileExtension(fileName) {
continue continue
} }
@ -335,6 +335,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
return nil return nil
} }
func isYamlFileExtension(fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
return ext == ".yaml" || ext == ".yml"
}
// k8sYamlStruct stubs a Kubernetes YAML file. // k8sYamlStruct stubs a Kubernetes YAML file.
type k8sYamlStruct struct { type k8sYamlStruct struct {
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`

@ -439,3 +439,23 @@ items:
t.Fatalf("List objects keep annotations should pass. got: %s", err) t.Fatalf("List objects keep annotations should pass. got: %s", err)
} }
} }
func TestIsYamlFileExtension(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
{"test.yaml", true},
{"test.yml", true},
{"test.txt", false},
{"test", false},
}
for _, test := range tests {
result := isYamlFileExtension(test.filename)
if result != test.expected {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -218,9 +218,10 @@ httpRoute:
# value: v2 # value: v2
resources: {} resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious # For publicly distributed charts, we recommend leaving 'resources' commented out.
# choice for the user. This also increases chances charts run on environments with little # This makes resource allocation a conscious choice for the user and increases the chances
# resources, such as Minikube. If you do want to specify resources, uncomment the following # charts run on a wide range of environments from low-resource clusters like Minikube to those
# with strict resource policies. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'. # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits: # limits:
# cpu: 100m # cpu: 100m

@ -85,10 +85,10 @@ func NewExtractor(source string) (Extractor, error) {
// //
// - The character `:` is considered illegal because it is a separator on UNIX and a // - The character `:` is considered illegal because it is a separator on UNIX and a
// drive designator on Windows. // drive designator on Windows.
// - The path component `..` is considered suspicions, and therefore illegal // - The path component `..` is considered suspicious, and therefore illegal
// - The character \ (backslash) is treated as a path separator and is converted to /. // - The character \ (backslash) is treated as a path separator and is converted to /.
// - Beginning a path with a path separator is illegal // - Beginning a path with a path separator is illegal
// - Rudimentary symlink protects are offered by SecureJoin. // - Rudimentary symlink protections are offered by SecureJoin.
func cleanJoin(root, dest string) (string, error) { func cleanJoin(root, dest string) (string, error) {
// On Windows, this is a drive separator. On UNIX-like, this is the path list separator. // On Windows, this is a drive separator. On UNIX-like, this is the path list separator.

@ -139,18 +139,24 @@ func Update(i Installer) error {
} }
// NewForSource determines the correct Installer for the given source. // NewForSource determines the correct Installer for the given source.
func NewForSource(source, version string) (Installer, error) { func NewForSource(source, version string) (installer Installer, err error) {
// Check if source is an OCI registry reference
if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) { if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) {
return NewOCIInstaller(source) // Source is an OCI registry reference
} installer, err = NewOCIInstaller(source)
// Check if source is a local directory } else if isLocalReference(source) {
if isLocalReference(source) { // Source is a local directory
return NewLocalInstaller(source) installer, err = NewLocalInstaller(source)
} else if isRemoteHTTPArchive(source) { } else if isRemoteHTTPArchive(source) {
return NewHTTPInstaller(source) installer, err = NewHTTPInstaller(source)
} else {
installer, err = NewVCSInstaller(source, version)
}
if err != nil {
return installer, fmt.Errorf("cannot get information about plugin source %q (if it's a local directory, does it exist?), last error was: %w", source, err)
} }
return NewVCSInstaller(source, version)
return
} }
// FindSource determines the correct Installer for the given source. // FindSource determines the correct Installer for the given source.

@ -29,8 +29,8 @@ import (
"helm.sh/helm/v4/pkg/helmpath" "helm.sh/helm/v4/pkg/helmpath"
) )
// ErrPluginNotAFolder indicates that the plugin path is not a folder. // ErrPluginNotADirectory indicates that the plugin path is not a directory.
var ErrPluginNotAFolder = errors.New("expected plugin to be a folder") var ErrPluginNotADirectory = errors.New("expected plugin to be a directory (containing a file 'plugin.yaml')")
// LocalInstaller installs plugins from the filesystem. // LocalInstaller installs plugins from the filesystem.
type LocalInstaller struct { type LocalInstaller struct {
@ -91,7 +91,7 @@ func (i *LocalInstaller) installFromDirectory() error {
return err return err
} }
if !stat.IsDir() { if !stat.IsDir() {
return ErrPluginNotAFolder return ErrPluginNotADirectory
} }
if !isPlugin(i.Source) { if !isPlugin(i.Source) {

@ -64,7 +64,7 @@ func TestLocalInstallerNotAFolder(t *testing.T) {
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
if err != ErrPluginNotAFolder { if err != ErrPluginNotADirectory {
t.Fatalf("expected error to equal: %q", err) t.Fatalf("expected error to equal: %q", err)
} }
} }

@ -73,27 +73,27 @@ type pluginTypeMeta struct {
var pluginTypes = []pluginTypeMeta{ var pluginTypes = []pluginTypeMeta{
{ {
pluginType: "test/v1", pluginType: "test/v1",
inputType: reflect.TypeOf(schema.InputMessageTestV1{}), inputType: reflect.TypeFor[schema.InputMessageTestV1](),
outputType: reflect.TypeOf(schema.OutputMessageTestV1{}), outputType: reflect.TypeFor[schema.OutputMessageTestV1](),
configType: reflect.TypeOf(schema.ConfigTestV1{}), configType: reflect.TypeFor[schema.ConfigTestV1](),
}, },
{ {
pluginType: "cli/v1", pluginType: "cli/v1",
inputType: reflect.TypeOf(schema.InputMessageCLIV1{}), inputType: reflect.TypeFor[schema.InputMessageCLIV1](),
outputType: reflect.TypeOf(schema.OutputMessageCLIV1{}), outputType: reflect.TypeFor[schema.OutputMessageCLIV1](),
configType: reflect.TypeOf(schema.ConfigCLIV1{}), configType: reflect.TypeFor[schema.ConfigCLIV1](),
}, },
{ {
pluginType: "getter/v1", pluginType: "getter/v1",
inputType: reflect.TypeOf(schema.InputMessageGetterV1{}), inputType: reflect.TypeFor[schema.InputMessageGetterV1](),
outputType: reflect.TypeOf(schema.OutputMessageGetterV1{}), outputType: reflect.TypeFor[schema.OutputMessageGetterV1](),
configType: reflect.TypeOf(schema.ConfigGetterV1{}), configType: reflect.TypeFor[schema.ConfigGetterV1](),
}, },
{ {
pluginType: "postrenderer/v1", pluginType: "postrenderer/v1",
inputType: reflect.TypeOf(schema.InputMessagePostRendererV1{}), inputType: reflect.TypeFor[schema.InputMessagePostRendererV1](),
outputType: reflect.TypeOf(schema.OutputMessagePostRendererV1{}), outputType: reflect.TypeFor[schema.OutputMessagePostRendererV1](),
configType: reflect.TypeOf(schema.ConfigPostRendererV1{}), configType: reflect.TypeFor[schema.ConfigPostRendererV1](),
}, },
} }

@ -71,7 +71,7 @@ func TestSubprocessPluginRuntime(t *testing.T) {
output, err := p.Invoke(t.Context(), &Input{ output, err := p.Invoke(t.Context(), &Input{
Message: schema.InputMessageCLIV1{ Message: schema.InputMessageCLIV1{
ExtraArgs: []string{"arg1", "arg2"}, ExtraArgs: []string{"arg1", "arg2"},
//Env: []string{"FOO=bar"}, // Env: []string{"FOO=bar"},
}, },
}) })

@ -70,7 +70,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
if err != nil { if err != nil {
return fmt.Errorf("error evaluating symlink %s: %w", path, err) return fmt.Errorf("error evaluating symlink %s: %w", path, err)
} }
//This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons. // This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved) slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved)
if info, err = os.Lstat(resolved); err != nil { if info, err = os.Lstat(resolved); err != nil {
return err return err

@ -18,6 +18,7 @@ package version // import "helm.sh/helm/v4/internal/version"
import ( import (
"flag" "flag"
"fmt"
"runtime" "runtime"
"strings" "strings"
) )
@ -37,6 +38,11 @@ var (
gitCommit = "" gitCommit = ""
// gitTreeState is the state of the git tree // gitTreeState is the state of the git tree
gitTreeState = "" gitTreeState = ""
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
// must be a string.
kubeClientVersionMajor = ""
kubeClientVersionMinor = ""
) )
// BuildInfo describes the compile time information. // BuildInfo describes the compile time information.
@ -49,6 +55,8 @@ type BuildInfo struct {
GitTreeState string `json:"git_tree_state,omitempty"` GitTreeState string `json:"git_tree_state,omitempty"`
// GoVersion is the version of the Go compiler used. // GoVersion is the version of the Go compiler used.
GoVersion string `json:"go_version,omitempty"` GoVersion string `json:"go_version,omitempty"`
// KubeClientVersion is the version of client-go Helm was build with
KubeClientVersion string `json:"kube_client_version"`
} }
// GetVersion returns the semver string of the version // GetVersion returns the semver string of the version
@ -67,10 +75,11 @@ func GetUserAgent() string {
// Get returns build info // Get returns build info
func Get() BuildInfo { func Get() BuildInfo {
v := BuildInfo{ v := BuildInfo{
Version: GetVersion(), Version: GetVersion(),
GitCommit: gitCommit, GitCommit: gitCommit,
GitTreeState: gitTreeState, GitTreeState: gitTreeState,
GoVersion: runtime.Version(), GoVersion: runtime.Version(),
KubeClientVersion: fmt.Sprintf("v%s.%s", kubeClientVersionMajor, kubeClientVersionMinor),
} }
// HACK(bacongobbler): strip out GoVersion during a test run for consistent test output // HACK(bacongobbler): strip out GoVersion during a test run for consistent test output

@ -156,7 +156,7 @@ func (t *templateLinter) Lint() {
t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName)) t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName))
// We only apply the following lint rules to yaml files // We only apply the following lint rules to yaml files
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" { if !isYamlFileExtension(fileName) {
continue continue
} }
@ -366,6 +366,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
return nil return nil
} }
func isYamlFileExtension(fileName string) bool {
ext := strings.ToLower(filepath.Ext(fileName))
return ext == ".yaml" || ext == ".yml"
}
// k8sYamlStruct stubs a Kubernetes YAML file. // k8sYamlStruct stubs a Kubernetes YAML file.
type k8sYamlStruct struct { type k8sYamlStruct struct {
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`

@ -462,3 +462,23 @@ items:
t.Fatalf("List objects keep annotations should pass. got: %s", err) t.Fatalf("List objects keep annotations should pass. got: %s", err)
} }
} }
func TestIsYamlFileExtension(t *testing.T) {
tests := []struct {
filename string
expected bool
}{
{"test.yaml", true},
{"test.yml", true},
{"test.txt", false},
{"test", false},
}
for _, test := range tests {
result := isYamlFileExtension(test.filename)
if result != test.expected {
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
}
}
}

@ -245,3 +245,373 @@ func TestListOutputCompletion(t *testing.T) {
func TestListFileCompletion(t *testing.T) { func TestListFileCompletion(t *testing.T) {
checkFileCompletion(t, "list", false) checkFileCompletion(t, "list", false)
} }
func TestListOutputFormats(t *testing.T) {
defaultNamespace := "default"
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releaseFixture := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []cmdTestCase{{
name: "list releases in json format",
cmd: "list --output json",
golden: "output/list-json.txt",
rels: releaseFixture,
}, {
name: "list releases in yaml format",
cmd: "list --output yaml",
golden: "output/list-yaml.txt",
rels: releaseFixture,
}}
runTestCmd(t, tests)
}
func TestReleaseListWriter(t *testing.T) {
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releases := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []struct {
name string
releases []*release.Release
timeFormat string
noHeaders bool
noColor bool
}{
{
name: "empty releases list",
releases: []*release.Release{},
timeFormat: "",
noHeaders: false,
noColor: false,
},
{
name: "custom time format",
releases: releases,
timeFormat: "2006-01-02",
noHeaders: false,
noColor: false,
},
{
name: "no headers",
releases: releases,
timeFormat: "",
noHeaders: true,
noColor: false,
},
{
name: "no color",
releases: releases,
timeFormat: "",
noHeaders: false,
noColor: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
writer := newReleaseListWriter(tt.releases, tt.timeFormat, tt.noHeaders, tt.noColor)
if writer == nil {
t.Error("Expected writer to be non-nil")
} else {
if len(writer.releases) != len(tt.releases) {
t.Errorf("Expected %d releases, got %d", len(tt.releases), len(writer.releases))
}
}
})
}
}
func TestReleaseListWriterMethods(t *testing.T) {
timestamp := time.Unix(1452902400, 0).UTC()
zeroTimestamp := time.Time{}
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releases := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
{
Name: "zero-time-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: zeroTimestamp,
Status: common.StatusFailed,
},
Chart: chartInfo,
},
}
tests := []struct {
name string
status common.Status
}{
{"deployed", common.StatusDeployed},
{"failed", common.StatusFailed},
{"pending-install", common.StatusPendingInstall},
{"pending-upgrade", common.StatusPendingUpgrade},
{"pending-rollback", common.StatusPendingRollback},
{"uninstalling", common.StatusUninstalling},
{"uninstalled", common.StatusUninstalled},
{"superseded", common.StatusSuperseded},
{"unknown", common.StatusUnknown},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
testReleases := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: "default",
Info: &release.Info{
LastDeployed: timestamp,
Status: tt.status,
},
Chart: chartInfo,
},
}
writer := newReleaseListWriter(testReleases, "", false, false)
var buf []byte
out := &bytesWriter{buf: &buf}
err := writer.WriteJSON(out)
if err != nil {
t.Errorf("WriteJSON failed: %v", err)
}
err = writer.WriteYAML(out)
if err != nil {
t.Errorf("WriteYAML failed: %v", err)
}
err = writer.WriteTable(out)
if err != nil {
t.Errorf("WriteTable failed: %v", err)
}
})
}
writer := newReleaseListWriter(releases, "", false, false)
var buf []byte
out := &bytesWriter{buf: &buf}
err := writer.WriteJSON(out)
if err != nil {
t.Errorf("WriteJSON failed: %v", err)
}
err = writer.WriteYAML(out)
if err != nil {
t.Errorf("WriteYAML failed: %v", err)
}
err = writer.WriteTable(out)
if err != nil {
t.Errorf("WriteTable failed: %v", err)
}
}
func TestFilterReleases(t *testing.T) {
releases := []*release.Release{
{Name: "release1"},
{Name: "release2"},
{Name: "release3"},
}
tests := []struct {
name string
releases []*release.Release
ignoredReleaseNames []string
expectedCount int
}{
{
name: "nil ignored list",
releases: releases,
ignoredReleaseNames: nil,
expectedCount: 3,
},
{
name: "empty ignored list",
releases: releases,
ignoredReleaseNames: []string{},
expectedCount: 3,
},
{
name: "filter one release",
releases: releases,
ignoredReleaseNames: []string{"release1"},
expectedCount: 2,
},
{
name: "filter multiple releases",
releases: releases,
ignoredReleaseNames: []string{"release1", "release3"},
expectedCount: 1,
},
{
name: "filter non-existent release",
releases: releases,
ignoredReleaseNames: []string{"non-existent"},
expectedCount: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := filterReleases(tt.releases, tt.ignoredReleaseNames)
if len(result) != tt.expectedCount {
t.Errorf("Expected %d releases, got %d", tt.expectedCount, len(result))
}
})
}
}
type bytesWriter struct {
buf *[]byte
}
func (b *bytesWriter) Write(p []byte) (n int, err error) {
*b.buf = append(*b.buf, p...)
return len(p), nil
}
func TestListCustomTimeFormat(t *testing.T) {
defaultNamespace := "default"
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
releaseFixture := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp,
Status: common.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []cmdTestCase{{
name: "list releases with custom time format",
cmd: "list --time-format '2006-01-02 15:04:05'",
golden: "output/list-time-format.txt",
rels: releaseFixture,
}}
runTestCmd(t, tests)
}
func TestListStatusMapping(t *testing.T) {
defaultNamespace := "default"
timestamp := time.Unix(1452902400, 0).UTC()
chartInfo := &chart.Chart{
Metadata: &chart.Metadata{
Name: "test-chart",
Version: "1.0.0",
AppVersion: "0.0.1",
},
}
testCases := []struct {
name string
status common.Status
}{
{"deployed", common.StatusDeployed},
{"failed", common.StatusFailed},
{"pending-install", common.StatusPendingInstall},
{"pending-upgrade", common.StatusPendingUpgrade},
{"pending-rollback", common.StatusPendingRollback},
{"uninstalling", common.StatusUninstalling},
{"uninstalled", common.StatusUninstalled},
{"superseded", common.StatusSuperseded},
{"unknown", common.StatusUnknown},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
releaseFixture := []*release.Release{
{
Name: "test-release",
Version: 1,
Namespace: defaultNamespace,
Info: &release.Info{
LastDeployed: timestamp,
Status: tc.status,
},
Chart: chartInfo,
},
}
writer := newReleaseListWriter(releaseFixture, "", false, false)
if len(writer.releases) != 1 {
t.Errorf("Expected 1 release, got %d", len(writer.releases))
}
if writer.releases[0].Status != tc.status.String() {
t.Errorf("Expected status %s, got %s", tc.status.String(), writer.releases[0].Status)
}
})
}
}

@ -287,7 +287,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string {
if isNotExist(err) { if isNotExist(err) {
// If there is no cached charts file, fallback to the full index file. // If there is no cached charts file, fallback to the full index file.
// This is much slower but can happen after the caching feature is first // This is much slower but can happen after the caching feature is first
// installed but before the user does a 'helm repo update' to generate the // installed but before the user does a 'helm repo update' to generate the
// first cached charts file. // first cached charts file.
path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName)) path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
if indexFile, err := repo.LoadIndexFile(path); err == nil { if indexFile, err := repo.LoadIndexFile(path); err == nil {

@ -0,0 +1 @@
[{"name":"test-release","namespace":"default","revision":"1","updated":"2016-01-16 00:00:00 +0000 UTC","status":"deployed","chart":"test-chart-1.0.0","app_version":"0.0.1"}]

@ -0,0 +1,2 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
test-release default 1 2016-01-16 00:00:00 deployed test-chart-1.0.0 0.0.1

@ -0,0 +1,7 @@
- app_version: 0.0.1
chart: test-chart-1.0.0
name: test-release
namespace: default
revision: "1"
status: deployed
updated: 2016-01-16 00:00:00 +0000 UTC

@ -1 +1 @@
version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:""} version.BuildInfo{Version:"v4.0", GitCommit:"", GitTreeState:"", GoVersion:"", KubeClientVersion:"v."}

@ -64,7 +64,7 @@ func (f files) Get(name string) string {
} }
// Glob takes a glob pattern and returns another files object only containing // Glob takes a glob pattern and returns another files object only containing
// matched files. // matched files.
// //
// This is designed to be called from a template. // This is designed to be called from a template.
// //

@ -520,11 +520,11 @@ func TestHTTPGetterTarDownload(t *testing.T) {
b := make([]byte, 512) b := make([]byte, 512)
f.Read(b) f.Read(b)
//Get the file size // Get the file size
FileStat, _ := f.Stat() FileStat, _ := f.Stat()
FileSize := strconv.FormatInt(FileStat.Size(), 10) FileSize := strconv.FormatInt(FileStat.Size(), 10)
//Simulating improper header values from bitbucket // Simulating improper header values from bitbucket
w.Header().Set("Content-Type", "application/x-tar") w.Header().Set("Content-Type", "application/x-tar")
w.Header().Set("Content-Encoding", "gzip") w.Header().Set("Content-Encoding", "gzip")
w.Header().Set("Content-Length", FileSize) w.Header().Set("Content-Length", FileSize)

@ -109,7 +109,7 @@ func (g *getterPlugin) Get(href string, options ...Option) (*bytes.Buffer, error
Protocol: u.Scheme, Protocol: u.Scheme,
}, },
// TODO should we pass Stdin, Stdout, and Stderr through Input here to getter plugins? // TODO should we pass Stdin, Stdout, and Stderr through Input here to getter plugins?
//Stdout: os.Stdout, // Stdout: os.Stdout,
} }
output, err := g.plg.Invoke(context.Background(), input) output, err := g.plg.Invoke(context.Background(), input)
if err != nil { if err != nil {

@ -573,10 +573,17 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA
} }
if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil { if err := deleteResource(info, metav1.DeletePropagationBackground); err != nil {
c.Logger().Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err)) c.Logger().Debug("failed to delete resource", "namespace", info.Namespace, "name", info.Name, "kind", info.Mapping.GroupVersionKind.Kind, slog.Any("error", err))
if !apierrors.IsNotFound(err) {
updateErrors = append(updateErrors, fmt.Errorf("failed to delete resource %s: %w", info.Name, err))
}
continue continue
} }
res.Deleted = append(res.Deleted, info) res.Deleted = append(res.Deleted, info)
} }
if len(updateErrors) != 0 {
return res, joinErrors(updateErrors, " && ")
}
return res, nil return res, nil
} }
@ -693,19 +700,19 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
errs = append(errs, o(&updateOptions)) errs = append(errs, o(&updateOptions))
} }
if err := errors.Join(errs...); err != nil { if err := errors.Join(errs...); err != nil {
return nil, fmt.Errorf("invalid client update option(s): %w", err) return &Result{}, fmt.Errorf("invalid client update option(s): %w", err)
} }
if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply { if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply {
return nil, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together") return &Result{}, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together")
} }
if updateOptions.forceConflicts && updateOptions.forceReplace { if updateOptions.forceConflicts && updateOptions.forceReplace {
return nil, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together") return &Result{}, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together")
} }
if updateOptions.serverSideApply && updateOptions.forceReplace { if updateOptions.serverSideApply && updateOptions.forceReplace {
return nil, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together") return &Result{}, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together")
} }
makeUpdateApplyFunc := func() UpdateApplyFunc { makeUpdateApplyFunc := func() UpdateApplyFunc {

@ -321,6 +321,7 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured bool ThreeWayMergeForUnstructured bool
ServerSideApply bool ServerSideApply bool
ExpectedActions []string ExpectedActions []string
ExpectedError string
} }
expectedActionsClientSideApply := []string{ expectedActionsClientSideApply := []string{
@ -336,6 +337,8 @@ func TestUpdate(t *testing.T) {
"/namespaces/default/pods:POST", // retry due to 409 "/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods/squid:GET", "/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE", "/namespaces/default/pods/squid:DELETE",
"/namespaces/default/pods/notfound:GET",
"/namespaces/default/pods/notfound:DELETE",
} }
expectedActionsServerSideApply := []string{ expectedActionsServerSideApply := []string{
@ -351,11 +354,13 @@ func TestUpdate(t *testing.T) {
"/namespaces/default/pods:POST", // retry due to 409 "/namespaces/default/pods:POST", // retry due to 409
"/namespaces/default/pods/squid:GET", "/namespaces/default/pods/squid:GET",
"/namespaces/default/pods/squid:DELETE", "/namespaces/default/pods/squid:DELETE",
"/namespaces/default/pods/notfound:GET",
"/namespaces/default/pods/notfound:DELETE",
} }
testCases := map[string]testCase{ testCases := map[string]testCase{
"client-side apply": { "client-side apply": {
OriginalPods: newPodList("starfish", "otter", "squid"), OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
TargetPods: func() v1.PodList { TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin") listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
@ -365,9 +370,10 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured: false, ThreeWayMergeForUnstructured: false,
ServerSideApply: false, ServerSideApply: false,
ExpectedActions: expectedActionsClientSideApply, ExpectedActions: expectedActionsClientSideApply,
ExpectedError: "",
}, },
"client-side apply (three-way merge for unstructured)": { "client-side apply (three-way merge for unstructured)": {
OriginalPods: newPodList("starfish", "otter", "squid"), OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
TargetPods: func() v1.PodList { TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin") listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
@ -377,9 +383,10 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured: true, ThreeWayMergeForUnstructured: true,
ServerSideApply: false, ServerSideApply: false,
ExpectedActions: expectedActionsClientSideApply, ExpectedActions: expectedActionsClientSideApply,
ExpectedError: "",
}, },
"serverSideApply": { "serverSideApply": {
OriginalPods: newPodList("starfish", "otter", "squid"), OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
TargetPods: func() v1.PodList { TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin") listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}} listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
@ -389,6 +396,23 @@ func TestUpdate(t *testing.T) {
ThreeWayMergeForUnstructured: false, ThreeWayMergeForUnstructured: false,
ServerSideApply: true, ServerSideApply: true,
ExpectedActions: expectedActionsServerSideApply, ExpectedActions: expectedActionsServerSideApply,
ExpectedError: "",
},
"serverSideApply with forbidden deletion": {
OriginalPods: newPodList("starfish", "otter", "squid", "notfound", "forbidden"),
TargetPods: func() v1.PodList {
listTarget := newPodList("starfish", "otter", "dolphin")
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
return listTarget
}(),
ThreeWayMergeForUnstructured: false,
ServerSideApply: true,
ExpectedActions: append(expectedActionsServerSideApply,
"/namespaces/default/pods/forbidden:GET",
"/namespaces/default/pods/forbidden:DELETE",
),
ExpectedError: "failed to delete resource forbidden:",
}, },
} }
@ -444,6 +468,22 @@ func TestUpdate(t *testing.T) {
return newResponse(http.StatusOK, &listTarget.Items[1]) return newResponse(http.StatusOK, &listTarget.Items[1])
case p == "/namespaces/default/pods/squid" && m == http.MethodGet: case p == "/namespaces/default/pods/squid" && m == http.MethodGet:
return newResponse(http.StatusOK, &listTarget.Items[2]) return newResponse(http.StatusOK, &listTarget.Items[2])
case p == "/namespaces/default/pods/notfound" && m == http.MethodGet:
// Resource exists in original but will simulate not found on delete
return newResponse(http.StatusOK, &listOriginal.Items[3])
case p == "/namespaces/default/pods/notfound" && m == http.MethodDelete:
// Simulate a not found during deletion; should not cause update to fail
return newResponse(http.StatusNotFound, notFoundBody())
case p == "/namespaces/default/pods/forbidden" && m == http.MethodGet:
return newResponse(http.StatusOK, &listOriginal.Items[4])
case p == "/namespaces/default/pods/forbidden" && m == http.MethodDelete:
// Simulate RBAC forbidden that should cause update to fail
return newResponse(http.StatusForbidden, &metav1.Status{
Status: metav1.StatusFailure,
Message: "pods \"forbidden\" is forbidden: User \"test-user\" cannot delete resource \"pods\" in API group \"\" in the namespace \"default\"",
Reason: metav1.StatusReasonForbidden,
Code: http.StatusForbidden,
})
default: default:
} }
@ -471,7 +511,13 @@ func TestUpdate(t *testing.T) {
ClientUpdateOptionForceReplace(false), ClientUpdateOptionForceReplace(false),
ClientUpdateOptionServerSideApply(tc.ServerSideApply, false), ClientUpdateOptionServerSideApply(tc.ServerSideApply, false),
ClientUpdateOptionUpgradeClientSideFieldManager(true)) ClientUpdateOptionUpgradeClientSideFieldManager(true))
require.NoError(t, err)
if tc.ExpectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.ExpectedError)
} else {
require.NoError(t, err)
}
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created)) assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated)) assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated))

@ -237,7 +237,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e
_, err = t.emptyVal() _, err = t.emptyVal()
return err return err
} }
//End of key. Consume =, Get value. // End of key. Consume =, Get value.
// FIXME: Get value list first // FIXME: Get value list first
vl, e := t.valList() vl, e := t.valList()
switch e { switch e {

@ -60,7 +60,7 @@ runAsRoot() {
# verifySupported checks that the os/arch combination is supported for # verifySupported checks that the os/arch combination is supported for
# binary builds. # binary builds.
verifySupported() { verifySupported() {
local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64" local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-loong64\nlinux-ppc64le\nlinux-s390x\nwindows-amd64\nwindows-arm64"
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
echo "No prebuilt binary for ${OS}-${ARCH}." echo "No prebuilt binary for ${OS}-${ARCH}."
echo "To build from source, go to https://github.com/helm/helm" echo "To build from source, go to https://github.com/helm/helm"

@ -69,7 +69,7 @@ runAsRoot() {
# verifySupported checks that the os/arch combination is supported for # verifySupported checks that the os/arch combination is supported for
# binary builds, as well whether or not necessary tools are present. # binary builds, as well whether or not necessary tools are present.
verifySupported() { verifySupported() {
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64" local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-loong64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
echo "No prebuilt binary for ${OS}-${ARCH}." echo "No prebuilt binary for ${OS}-${ARCH}."
echo "To build from source, go to https://github.com/helm/helm" echo "To build from source, go to https://github.com/helm/helm"

@ -87,6 +87,7 @@ Download Helm ${RELEASE}. The common platform binaries are here:
- [Linux arm](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm.tar.gz.sha256)) - [Linux arm](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm.tar.gz.sha256))
- [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256)) - [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256))
- [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256)) - [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256))
- [Linux loong64](https://get.helm.sh/helm-${RELEASE}-linux-loong64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-loong64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-loong64.tar.gz.sha256))
- [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256)) - [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256))
- [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-s390x.tar.gz.sha256)) - [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-s390x.tar.gz.sha256))
- [Linux riscv64](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-riscv64.tar.gz.sha256)) - [Linux riscv64](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-riscv64.tar.gz.sha256))

Loading…
Cancel
Save