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](),
}, },
} }

@ -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
@ -71,6 +79,7 @@ func Get() BuildInfo {
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)
}
})
}
}

@ -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."}

@ -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))
if tc.ExpectedError != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.ExpectedError)
} else {
require.NoError(t, err) 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))

@ -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