mirror of
https://github.com/helm/helm.git
synced 2026-05-28 04:35:48 -04:00
Merge remote-tracking branch 'upstream/main' into em/reinstate-logger-param
This commit is contained in:
commit
0f90c83118
29 changed files with 559 additions and 57 deletions
6
Makefile
6
Makefile
|
|
@ -1,8 +1,8 @@
|
|||
BINDIR := $(CURDIR)/bin
|
||||
INSTALL_PATH ?= /usr/local/bin
|
||||
DIST_DIRS := find * -type d -exec
|
||||
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
|
||||
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
|
||||
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/loong64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64 windows/arm64
|
||||
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.tar.gz.sha256sum darwin-arm64.tar.gz darwin-arm64.tar.gz.sha256 darwin-arm64.tar.gz.sha256sum linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-amd64.tar.gz.sha256sum linux-386.tar.gz linux-386.tar.gz.sha256 linux-386.tar.gz.sha256sum linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm.tar.gz.sha256sum linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-arm64.tar.gz.sha256sum linux-loong64.tar.gz linux-loong64.tar.gz.sha256 linux-loong64.tar.gz.sha256sum linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 linux-ppc64le.tar.gz.sha256sum linux-s390x.tar.gz linux-s390x.tar.gz.sha256 linux-s390x.tar.gz.sha256sum linux-riscv64.tar.gz linux-riscv64.tar.gz.sha256 linux-riscv64.tar.gz.sha256sum windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum windows-arm64.zip windows-arm64.zip.sha256 windows-arm64.zip.sha256sum
|
||||
BINNAME ?= helm
|
||||
|
||||
GOBIN = $(shell go env GOBIN)
|
||||
|
|
@ -69,6 +69,8 @@ LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMajor=$(K8S_M
|
|||
LDFLAGS += -X helm.sh/helm/v4/pkg/internal/v3/lint/rules.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
|
||||
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMajor=$(K8S_MODULES_MAJOR_VER)
|
||||
LDFLAGS += -X helm.sh/helm/v4/pkg/chart/common/util.k8sVersionMinor=$(K8S_MODULES_MINOR_VER)
|
||||
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMajor=$(K8S_MODULES_MAJOR_VER)
|
||||
LDFLAGS += -X helm.sh/helm/v4/internal/version.kubeClientVersionMinor=$(K8S_MODULES_MINOR_VER)
|
||||
|
||||
.PHONY: all
|
||||
all: build
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ func TemplatesWithSkipSchemaValidation(linter *support.Linter, values map[string
|
|||
linter.RunLinterRule(support.ErrorSev, fpath, validateAllowedExtension(fileName))
|
||||
|
||||
// We only apply the following lint rules to yaml files
|
||||
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
|
||||
if !isYamlFileExtension(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -335,6 +335,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func isYamlFileExtension(fileName string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
return ext == ".yaml" || ext == ".yml"
|
||||
}
|
||||
|
||||
// k8sYamlStruct stubs a Kubernetes YAML file.
|
||||
type k8sYamlStruct struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
|
|
|
|||
|
|
@ -439,3 +439,23 @@ items:
|
|||
t.Fatalf("List objects keep annotations should pass. got: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsYamlFileExtension(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
expected bool
|
||||
}{
|
||||
{"test.yaml", true},
|
||||
{"test.yml", true},
|
||||
{"test.txt", false},
|
||||
{"test", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := isYamlFileExtension(test.filename)
|
||||
if result != test.expected {
|
||||
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,9 +218,10 @@ httpRoute:
|
|||
# value: v2
|
||||
|
||||
resources: {}
|
||||
# We usually recommend not to specify default resources and to leave this as a conscious
|
||||
# choice for the user. This also increases chances charts run on environments with little
|
||||
# resources, such as Minikube. If you do want to specify resources, uncomment the following
|
||||
# For publicly distributed charts, we recommend leaving 'resources' commented out.
|
||||
# This makes resource allocation a conscious choice for the user and increases the chances
|
||||
# charts run on a wide range of environments from low-resource clusters like Minikube to those
|
||||
# with strict resource policies. If you do want to specify resources, uncomment the following
|
||||
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
|
||||
# limits:
|
||||
# cpu: 100m
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ func NewExtractor(source string) (Extractor, error) {
|
|||
//
|
||||
// - The character `:` is considered illegal because it is a separator on UNIX and a
|
||||
// drive designator on Windows.
|
||||
// - The path component `..` is considered suspicions, and therefore illegal
|
||||
// - The path component `..` is considered suspicious, and therefore illegal
|
||||
// - The character \ (backslash) is treated as a path separator and is converted to /.
|
||||
// - Beginning a path with a path separator is illegal
|
||||
// - Rudimentary symlink protects are offered by SecureJoin.
|
||||
// - Rudimentary symlink protections are offered by SecureJoin.
|
||||
func cleanJoin(root, dest string) (string, error) {
|
||||
|
||||
// On Windows, this is a drive separator. On UNIX-like, this is the path list separator.
|
||||
|
|
|
|||
|
|
@ -139,18 +139,24 @@ func Update(i Installer) error {
|
|||
}
|
||||
|
||||
// NewForSource determines the correct Installer for the given source.
|
||||
func NewForSource(source, version string) (Installer, error) {
|
||||
// Check if source is an OCI registry reference
|
||||
func NewForSource(source, version string) (installer Installer, err error) {
|
||||
if strings.HasPrefix(source, fmt.Sprintf("%s://", registry.OCIScheme)) {
|
||||
return NewOCIInstaller(source)
|
||||
}
|
||||
// Check if source is a local directory
|
||||
if isLocalReference(source) {
|
||||
return NewLocalInstaller(source)
|
||||
// Source is an OCI registry reference
|
||||
installer, err = NewOCIInstaller(source)
|
||||
} else if isLocalReference(source) {
|
||||
// Source is a local directory
|
||||
installer, err = NewLocalInstaller(source)
|
||||
} else if isRemoteHTTPArchive(source) {
|
||||
return NewHTTPInstaller(source)
|
||||
installer, err = NewHTTPInstaller(source)
|
||||
} else {
|
||||
installer, err = NewVCSInstaller(source, version)
|
||||
}
|
||||
return 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
|
||||
}
|
||||
|
||||
// FindSource determines the correct Installer for the given source.
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ import (
|
|||
"helm.sh/helm/v4/pkg/helmpath"
|
||||
)
|
||||
|
||||
// ErrPluginNotAFolder indicates that the plugin path is not a folder.
|
||||
var ErrPluginNotAFolder = errors.New("expected plugin to be a folder")
|
||||
// ErrPluginNotADirectory indicates that the plugin path is not a directory.
|
||||
var ErrPluginNotADirectory = errors.New("expected plugin to be a directory (containing a file 'plugin.yaml')")
|
||||
|
||||
// LocalInstaller installs plugins from the filesystem.
|
||||
type LocalInstaller struct {
|
||||
|
|
@ -91,7 +91,7 @@ func (i *LocalInstaller) installFromDirectory() error {
|
|||
return err
|
||||
}
|
||||
if !stat.IsDir() {
|
||||
return ErrPluginNotAFolder
|
||||
return ErrPluginNotADirectory
|
||||
}
|
||||
|
||||
if !isPlugin(i.Source) {
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ func TestLocalInstallerNotAFolder(t *testing.T) {
|
|||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if err != ErrPluginNotAFolder {
|
||||
if err != ErrPluginNotADirectory {
|
||||
t.Fatalf("expected error to equal: %q", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,27 +73,27 @@ type pluginTypeMeta struct {
|
|||
var pluginTypes = []pluginTypeMeta{
|
||||
{
|
||||
pluginType: "test/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessageTestV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessageTestV1{}),
|
||||
configType: reflect.TypeOf(schema.ConfigTestV1{}),
|
||||
inputType: reflect.TypeFor[schema.InputMessageTestV1](),
|
||||
outputType: reflect.TypeFor[schema.OutputMessageTestV1](),
|
||||
configType: reflect.TypeFor[schema.ConfigTestV1](),
|
||||
},
|
||||
{
|
||||
pluginType: "cli/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessageCLIV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessageCLIV1{}),
|
||||
configType: reflect.TypeOf(schema.ConfigCLIV1{}),
|
||||
inputType: reflect.TypeFor[schema.InputMessageCLIV1](),
|
||||
outputType: reflect.TypeFor[schema.OutputMessageCLIV1](),
|
||||
configType: reflect.TypeFor[schema.ConfigCLIV1](),
|
||||
},
|
||||
{
|
||||
pluginType: "getter/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessageGetterV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessageGetterV1{}),
|
||||
configType: reflect.TypeOf(schema.ConfigGetterV1{}),
|
||||
inputType: reflect.TypeFor[schema.InputMessageGetterV1](),
|
||||
outputType: reflect.TypeFor[schema.OutputMessageGetterV1](),
|
||||
configType: reflect.TypeFor[schema.ConfigGetterV1](),
|
||||
},
|
||||
{
|
||||
pluginType: "postrenderer/v1",
|
||||
inputType: reflect.TypeOf(schema.InputMessagePostRendererV1{}),
|
||||
outputType: reflect.TypeOf(schema.OutputMessagePostRendererV1{}),
|
||||
configType: reflect.TypeOf(schema.ConfigPostRendererV1{}),
|
||||
inputType: reflect.TypeFor[schema.InputMessagePostRendererV1](),
|
||||
outputType: reflect.TypeFor[schema.OutputMessagePostRendererV1](),
|
||||
configType: reflect.TypeFor[schema.ConfigPostRendererV1](),
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func TestSubprocessPluginRuntime(t *testing.T) {
|
|||
output, err := p.Invoke(t.Context(), &Input{
|
||||
Message: schema.InputMessageCLIV1{
|
||||
ExtraArgs: []string{"arg1", "arg2"},
|
||||
//Env: []string{"FOO=bar"},
|
||||
// Env: []string{"FOO=bar"},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ func symwalk(path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("error evaluating symlink %s: %w", path, err)
|
||||
}
|
||||
//This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
|
||||
// This log message is to highlight a symlink that is being used within a chart, symlinks can be used for nefarious reasons.
|
||||
slog.Info("found symbolic link in path. Contents of linked file included and used", "path", path, "resolved", resolved)
|
||||
if info, err = os.Lstat(resolved); err != nil {
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ package version // import "helm.sh/helm/v4/internal/version"
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
|
@ -37,6 +38,11 @@ var (
|
|||
gitCommit = ""
|
||||
// gitTreeState is the state of the git tree
|
||||
gitTreeState = ""
|
||||
|
||||
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
|
||||
// must be a string.
|
||||
kubeClientVersionMajor = ""
|
||||
kubeClientVersionMinor = ""
|
||||
)
|
||||
|
||||
// BuildInfo describes the compile time information.
|
||||
|
|
@ -49,6 +55,8 @@ type BuildInfo struct {
|
|||
GitTreeState string `json:"git_tree_state,omitempty"`
|
||||
// GoVersion is the version of the Go compiler used.
|
||||
GoVersion string `json:"go_version,omitempty"`
|
||||
// KubeClientVersion is the version of client-go Helm was build with
|
||||
KubeClientVersion string `json:"kube_client_version"`
|
||||
}
|
||||
|
||||
// GetVersion returns the semver string of the version
|
||||
|
|
@ -67,10 +75,11 @@ func GetUserAgent() string {
|
|||
// Get returns build info
|
||||
func Get() BuildInfo {
|
||||
v := BuildInfo{
|
||||
Version: GetVersion(),
|
||||
GitCommit: gitCommit,
|
||||
GitTreeState: gitTreeState,
|
||||
GoVersion: runtime.Version(),
|
||||
Version: GetVersion(),
|
||||
GitCommit: gitCommit,
|
||||
GitTreeState: gitTreeState,
|
||||
GoVersion: runtime.Version(),
|
||||
KubeClientVersion: fmt.Sprintf("v%s.%s", kubeClientVersionMajor, kubeClientVersionMinor),
|
||||
}
|
||||
|
||||
// HACK(bacongobbler): strip out GoVersion during a test run for consistent test output
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ func (t *templateLinter) Lint() {
|
|||
t.linter.RunLinterRule(support.ErrorSev, fileName, validateAllowedExtension(fileName))
|
||||
|
||||
// We only apply the following lint rules to yaml files
|
||||
if filepath.Ext(fileName) != ".yaml" || filepath.Ext(fileName) == ".yml" {
|
||||
if !isYamlFileExtension(fileName) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
@ -366,6 +366,11 @@ func validateListAnnotations(yamlStruct *k8sYamlStruct, manifest string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func isYamlFileExtension(fileName string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(fileName))
|
||||
return ext == ".yaml" || ext == ".yml"
|
||||
}
|
||||
|
||||
// k8sYamlStruct stubs a Kubernetes YAML file.
|
||||
type k8sYamlStruct struct {
|
||||
APIVersion string `json:"apiVersion"`
|
||||
|
|
|
|||
|
|
@ -462,3 +462,23 @@ items:
|
|||
t.Fatalf("List objects keep annotations should pass. got: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsYamlFileExtension(t *testing.T) {
|
||||
tests := []struct {
|
||||
filename string
|
||||
expected bool
|
||||
}{
|
||||
{"test.yaml", true},
|
||||
{"test.yml", true},
|
||||
{"test.txt", false},
|
||||
{"test", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := isYamlFileExtension(test.filename)
|
||||
if result != test.expected {
|
||||
t.Errorf("isYamlFileExtension(%s) = %v; want %v", test.filename, result, test.expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -245,3 +245,373 @@ func TestListOutputCompletion(t *testing.T) {
|
|||
func TestListFileCompletion(t *testing.T) {
|
||||
checkFileCompletion(t, "list", false)
|
||||
}
|
||||
|
||||
func TestListOutputFormats(t *testing.T) {
|
||||
defaultNamespace := "default"
|
||||
timestamp := time.Unix(1452902400, 0).UTC()
|
||||
chartInfo := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "test-chart",
|
||||
Version: "1.0.0",
|
||||
AppVersion: "0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
releaseFixture := []*release.Release{
|
||||
{
|
||||
Name: "test-release",
|
||||
Version: 1,
|
||||
Namespace: defaultNamespace,
|
||||
Info: &release.Info{
|
||||
LastDeployed: timestamp,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []cmdTestCase{{
|
||||
name: "list releases in json format",
|
||||
cmd: "list --output json",
|
||||
golden: "output/list-json.txt",
|
||||
rels: releaseFixture,
|
||||
}, {
|
||||
name: "list releases in yaml format",
|
||||
cmd: "list --output yaml",
|
||||
golden: "output/list-yaml.txt",
|
||||
rels: releaseFixture,
|
||||
}}
|
||||
runTestCmd(t, tests)
|
||||
}
|
||||
|
||||
func TestReleaseListWriter(t *testing.T) {
|
||||
timestamp := time.Unix(1452902400, 0).UTC()
|
||||
chartInfo := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "test-chart",
|
||||
Version: "1.0.0",
|
||||
AppVersion: "0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
releases := []*release.Release{
|
||||
{
|
||||
Name: "test-release",
|
||||
Version: 1,
|
||||
Namespace: "default",
|
||||
Info: &release.Info{
|
||||
LastDeployed: timestamp,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
releases []*release.Release
|
||||
timeFormat string
|
||||
noHeaders bool
|
||||
noColor bool
|
||||
}{
|
||||
{
|
||||
name: "empty releases list",
|
||||
releases: []*release.Release{},
|
||||
timeFormat: "",
|
||||
noHeaders: false,
|
||||
noColor: false,
|
||||
},
|
||||
{
|
||||
name: "custom time format",
|
||||
releases: releases,
|
||||
timeFormat: "2006-01-02",
|
||||
noHeaders: false,
|
||||
noColor: false,
|
||||
},
|
||||
{
|
||||
name: "no headers",
|
||||
releases: releases,
|
||||
timeFormat: "",
|
||||
noHeaders: true,
|
||||
noColor: false,
|
||||
},
|
||||
{
|
||||
name: "no color",
|
||||
releases: releases,
|
||||
timeFormat: "",
|
||||
noHeaders: false,
|
||||
noColor: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
writer := newReleaseListWriter(tt.releases, tt.timeFormat, tt.noHeaders, tt.noColor)
|
||||
|
||||
if writer == nil {
|
||||
t.Error("Expected writer to be non-nil")
|
||||
} else {
|
||||
if len(writer.releases) != len(tt.releases) {
|
||||
t.Errorf("Expected %d releases, got %d", len(tt.releases), len(writer.releases))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseListWriterMethods(t *testing.T) {
|
||||
timestamp := time.Unix(1452902400, 0).UTC()
|
||||
zeroTimestamp := time.Time{}
|
||||
chartInfo := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "test-chart",
|
||||
Version: "1.0.0",
|
||||
AppVersion: "0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
releases := []*release.Release{
|
||||
{
|
||||
Name: "test-release",
|
||||
Version: 1,
|
||||
Namespace: "default",
|
||||
Info: &release.Info{
|
||||
LastDeployed: timestamp,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
{
|
||||
Name: "zero-time-release",
|
||||
Version: 1,
|
||||
Namespace: "default",
|
||||
Info: &release.Info{
|
||||
LastDeployed: zeroTimestamp,
|
||||
Status: common.StatusFailed,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status common.Status
|
||||
}{
|
||||
{"deployed", common.StatusDeployed},
|
||||
{"failed", common.StatusFailed},
|
||||
{"pending-install", common.StatusPendingInstall},
|
||||
{"pending-upgrade", common.StatusPendingUpgrade},
|
||||
{"pending-rollback", common.StatusPendingRollback},
|
||||
{"uninstalling", common.StatusUninstalling},
|
||||
{"uninstalled", common.StatusUninstalled},
|
||||
{"superseded", common.StatusSuperseded},
|
||||
{"unknown", common.StatusUnknown},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testReleases := []*release.Release{
|
||||
{
|
||||
Name: "test-release",
|
||||
Version: 1,
|
||||
Namespace: "default",
|
||||
Info: &release.Info{
|
||||
LastDeployed: timestamp,
|
||||
Status: tt.status,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
}
|
||||
|
||||
writer := newReleaseListWriter(testReleases, "", false, false)
|
||||
|
||||
var buf []byte
|
||||
out := &bytesWriter{buf: &buf}
|
||||
|
||||
err := writer.WriteJSON(out)
|
||||
if err != nil {
|
||||
t.Errorf("WriteJSON failed: %v", err)
|
||||
}
|
||||
|
||||
err = writer.WriteYAML(out)
|
||||
if err != nil {
|
||||
t.Errorf("WriteYAML failed: %v", err)
|
||||
}
|
||||
|
||||
err = writer.WriteTable(out)
|
||||
if err != nil {
|
||||
t.Errorf("WriteTable failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
writer := newReleaseListWriter(releases, "", false, false)
|
||||
|
||||
var buf []byte
|
||||
out := &bytesWriter{buf: &buf}
|
||||
|
||||
err := writer.WriteJSON(out)
|
||||
if err != nil {
|
||||
t.Errorf("WriteJSON failed: %v", err)
|
||||
}
|
||||
|
||||
err = writer.WriteYAML(out)
|
||||
if err != nil {
|
||||
t.Errorf("WriteYAML failed: %v", err)
|
||||
}
|
||||
|
||||
err = writer.WriteTable(out)
|
||||
if err != nil {
|
||||
t.Errorf("WriteTable failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterReleases(t *testing.T) {
|
||||
releases := []*release.Release{
|
||||
{Name: "release1"},
|
||||
{Name: "release2"},
|
||||
{Name: "release3"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
releases []*release.Release
|
||||
ignoredReleaseNames []string
|
||||
expectedCount int
|
||||
}{
|
||||
{
|
||||
name: "nil ignored list",
|
||||
releases: releases,
|
||||
ignoredReleaseNames: nil,
|
||||
expectedCount: 3,
|
||||
},
|
||||
{
|
||||
name: "empty ignored list",
|
||||
releases: releases,
|
||||
ignoredReleaseNames: []string{},
|
||||
expectedCount: 3,
|
||||
},
|
||||
{
|
||||
name: "filter one release",
|
||||
releases: releases,
|
||||
ignoredReleaseNames: []string{"release1"},
|
||||
expectedCount: 2,
|
||||
},
|
||||
{
|
||||
name: "filter multiple releases",
|
||||
releases: releases,
|
||||
ignoredReleaseNames: []string{"release1", "release3"},
|
||||
expectedCount: 1,
|
||||
},
|
||||
{
|
||||
name: "filter non-existent release",
|
||||
releases: releases,
|
||||
ignoredReleaseNames: []string{"non-existent"},
|
||||
expectedCount: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := filterReleases(tt.releases, tt.ignoredReleaseNames)
|
||||
if len(result) != tt.expectedCount {
|
||||
t.Errorf("Expected %d releases, got %d", tt.expectedCount, len(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type bytesWriter struct {
|
||||
buf *[]byte
|
||||
}
|
||||
|
||||
func (b *bytesWriter) Write(p []byte) (n int, err error) {
|
||||
*b.buf = append(*b.buf, p...)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func TestListCustomTimeFormat(t *testing.T) {
|
||||
defaultNamespace := "default"
|
||||
timestamp := time.Unix(1452902400, 0).UTC()
|
||||
chartInfo := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "test-chart",
|
||||
Version: "1.0.0",
|
||||
AppVersion: "0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
releaseFixture := []*release.Release{
|
||||
{
|
||||
Name: "test-release",
|
||||
Version: 1,
|
||||
Namespace: defaultNamespace,
|
||||
Info: &release.Info{
|
||||
LastDeployed: timestamp,
|
||||
Status: common.StatusDeployed,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
}
|
||||
|
||||
tests := []cmdTestCase{{
|
||||
name: "list releases with custom time format",
|
||||
cmd: "list --time-format '2006-01-02 15:04:05'",
|
||||
golden: "output/list-time-format.txt",
|
||||
rels: releaseFixture,
|
||||
}}
|
||||
runTestCmd(t, tests)
|
||||
}
|
||||
|
||||
func TestListStatusMapping(t *testing.T) {
|
||||
defaultNamespace := "default"
|
||||
timestamp := time.Unix(1452902400, 0).UTC()
|
||||
chartInfo := &chart.Chart{
|
||||
Metadata: &chart.Metadata{
|
||||
Name: "test-chart",
|
||||
Version: "1.0.0",
|
||||
AppVersion: "0.0.1",
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
status common.Status
|
||||
}{
|
||||
{"deployed", common.StatusDeployed},
|
||||
{"failed", common.StatusFailed},
|
||||
{"pending-install", common.StatusPendingInstall},
|
||||
{"pending-upgrade", common.StatusPendingUpgrade},
|
||||
{"pending-rollback", common.StatusPendingRollback},
|
||||
{"uninstalling", common.StatusUninstalling},
|
||||
{"uninstalled", common.StatusUninstalled},
|
||||
{"superseded", common.StatusSuperseded},
|
||||
{"unknown", common.StatusUnknown},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
releaseFixture := []*release.Release{
|
||||
{
|
||||
Name: "test-release",
|
||||
Version: 1,
|
||||
Namespace: defaultNamespace,
|
||||
Info: &release.Info{
|
||||
LastDeployed: timestamp,
|
||||
Status: tc.status,
|
||||
},
|
||||
Chart: chartInfo,
|
||||
},
|
||||
}
|
||||
|
||||
writer := newReleaseListWriter(releaseFixture, "", false, false)
|
||||
if len(writer.releases) != 1 {
|
||||
t.Errorf("Expected 1 release, got %d", len(writer.releases))
|
||||
}
|
||||
|
||||
if writer.releases[0].Status != tc.status.String() {
|
||||
t.Errorf("Expected status %s, got %s", tc.status.String(), writer.releases[0].Status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ func compListChartsOfRepo(repoName string, prefix string) []string {
|
|||
if isNotExist(err) {
|
||||
// If there is no cached charts file, fallback to the full index file.
|
||||
// This is much slower but can happen after the caching feature is first
|
||||
// installed but before the user does a 'helm repo update' to generate the
|
||||
// installed but before the user does a 'helm repo update' to generate the
|
||||
// first cached charts file.
|
||||
path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
|
||||
if indexFile, err := repo.LoadIndexFile(path); err == nil {
|
||||
|
|
|
|||
1
pkg/cmd/testdata/output/list-json.txt
vendored
Normal file
1
pkg/cmd/testdata/output/list-json.txt
vendored
Normal file
|
|
@ -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"}]
|
||||
2
pkg/cmd/testdata/output/list-time-format.txt
vendored
Normal file
2
pkg/cmd/testdata/output/list-time-format.txt
vendored
Normal file
|
|
@ -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
|
||||
7
pkg/cmd/testdata/output/list-yaml.txt
vendored
Normal file
7
pkg/cmd/testdata/output/list-yaml.txt
vendored
Normal file
|
|
@ -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
|
||||
2
pkg/cmd/testdata/output/version.txt
vendored
2
pkg/cmd/testdata/output/version.txt
vendored
|
|
@ -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
|
||||
// matched files.
|
||||
// matched files.
|
||||
//
|
||||
// This is designed to be called from a template.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -520,11 +520,11 @@ func TestHTTPGetterTarDownload(t *testing.T) {
|
|||
|
||||
b := make([]byte, 512)
|
||||
f.Read(b)
|
||||
//Get the file size
|
||||
// Get the file size
|
||||
FileStat, _ := f.Stat()
|
||||
FileSize := strconv.FormatInt(FileStat.Size(), 10)
|
||||
|
||||
//Simulating improper header values from bitbucket
|
||||
// Simulating improper header values from bitbucket
|
||||
w.Header().Set("Content-Type", "application/x-tar")
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
w.Header().Set("Content-Length", FileSize)
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ func (g *getterPlugin) Get(href string, options ...Option) (*bytes.Buffer, error
|
|||
Protocol: u.Scheme,
|
||||
},
|
||||
// TODO should we pass Stdin, Stdout, and Stderr through Input here to getter plugins?
|
||||
//Stdout: os.Stdout,
|
||||
// Stdout: os.Stdout,
|
||||
}
|
||||
output, err := g.plg.Invoke(context.Background(), input)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -573,10 +573,17 @@ func (c *Client) update(originals, targets ResourceList, updateApplyFunc UpdateA
|
|||
}
|
||||
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))
|
||||
if !apierrors.IsNotFound(err) {
|
||||
updateErrors = append(updateErrors, fmt.Errorf("failed to delete resource %s: %w", info.Name, err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
res.Deleted = append(res.Deleted, info)
|
||||
}
|
||||
|
||||
if len(updateErrors) != 0 {
|
||||
return res, joinErrors(updateErrors, " && ")
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
|
|
@ -693,19 +700,19 @@ func (c *Client) Update(originals, targets ResourceList, options ...ClientUpdate
|
|||
errs = append(errs, o(&updateOptions))
|
||||
}
|
||||
if err := errors.Join(errs...); err != nil {
|
||||
return nil, fmt.Errorf("invalid client update option(s): %w", err)
|
||||
return &Result{}, fmt.Errorf("invalid client update option(s): %w", err)
|
||||
}
|
||||
|
||||
if updateOptions.threeWayMergeForUnstructured && updateOptions.serverSideApply {
|
||||
return nil, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together")
|
||||
return &Result{}, fmt.Errorf("invalid operation: cannot use three-way merge for unstructured and server-side apply together")
|
||||
}
|
||||
|
||||
if updateOptions.forceConflicts && updateOptions.forceReplace {
|
||||
return nil, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together")
|
||||
return &Result{}, fmt.Errorf("invalid operation: cannot use force conflicts and force replace together")
|
||||
}
|
||||
|
||||
if updateOptions.serverSideApply && updateOptions.forceReplace {
|
||||
return nil, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together")
|
||||
return &Result{}, fmt.Errorf("invalid operation: cannot use server-side apply and force replace together")
|
||||
}
|
||||
|
||||
makeUpdateApplyFunc := func() UpdateApplyFunc {
|
||||
|
|
|
|||
|
|
@ -321,6 +321,7 @@ func TestUpdate(t *testing.T) {
|
|||
ThreeWayMergeForUnstructured bool
|
||||
ServerSideApply bool
|
||||
ExpectedActions []string
|
||||
ExpectedError string
|
||||
}
|
||||
|
||||
expectedActionsClientSideApply := []string{
|
||||
|
|
@ -336,6 +337,8 @@ func TestUpdate(t *testing.T) {
|
|||
"/namespaces/default/pods:POST", // retry due to 409
|
||||
"/namespaces/default/pods/squid:GET",
|
||||
"/namespaces/default/pods/squid:DELETE",
|
||||
"/namespaces/default/pods/notfound:GET",
|
||||
"/namespaces/default/pods/notfound:DELETE",
|
||||
}
|
||||
|
||||
expectedActionsServerSideApply := []string{
|
||||
|
|
@ -351,11 +354,13 @@ func TestUpdate(t *testing.T) {
|
|||
"/namespaces/default/pods:POST", // retry due to 409
|
||||
"/namespaces/default/pods/squid:GET",
|
||||
"/namespaces/default/pods/squid:DELETE",
|
||||
"/namespaces/default/pods/notfound:GET",
|
||||
"/namespaces/default/pods/notfound:DELETE",
|
||||
}
|
||||
|
||||
testCases := map[string]testCase{
|
||||
"client-side apply": {
|
||||
OriginalPods: newPodList("starfish", "otter", "squid"),
|
||||
OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
|
||||
TargetPods: func() v1.PodList {
|
||||
listTarget := newPodList("starfish", "otter", "dolphin")
|
||||
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
|
||||
|
|
@ -365,9 +370,10 @@ func TestUpdate(t *testing.T) {
|
|||
ThreeWayMergeForUnstructured: false,
|
||||
ServerSideApply: false,
|
||||
ExpectedActions: expectedActionsClientSideApply,
|
||||
ExpectedError: "",
|
||||
},
|
||||
"client-side apply (three-way merge for unstructured)": {
|
||||
OriginalPods: newPodList("starfish", "otter", "squid"),
|
||||
OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
|
||||
TargetPods: func() v1.PodList {
|
||||
listTarget := newPodList("starfish", "otter", "dolphin")
|
||||
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
|
||||
|
|
@ -377,9 +383,10 @@ func TestUpdate(t *testing.T) {
|
|||
ThreeWayMergeForUnstructured: true,
|
||||
ServerSideApply: false,
|
||||
ExpectedActions: expectedActionsClientSideApply,
|
||||
ExpectedError: "",
|
||||
},
|
||||
"serverSideApply": {
|
||||
OriginalPods: newPodList("starfish", "otter", "squid"),
|
||||
OriginalPods: newPodList("starfish", "otter", "squid", "notfound"),
|
||||
TargetPods: func() v1.PodList {
|
||||
listTarget := newPodList("starfish", "otter", "dolphin")
|
||||
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
|
||||
|
|
@ -389,6 +396,23 @@ func TestUpdate(t *testing.T) {
|
|||
ThreeWayMergeForUnstructured: false,
|
||||
ServerSideApply: true,
|
||||
ExpectedActions: expectedActionsServerSideApply,
|
||||
ExpectedError: "",
|
||||
},
|
||||
"serverSideApply with forbidden deletion": {
|
||||
OriginalPods: newPodList("starfish", "otter", "squid", "notfound", "forbidden"),
|
||||
TargetPods: func() v1.PodList {
|
||||
listTarget := newPodList("starfish", "otter", "dolphin")
|
||||
listTarget.Items[0].Spec.Containers[0].Ports = []v1.ContainerPort{{Name: "https", ContainerPort: 443}}
|
||||
|
||||
return listTarget
|
||||
}(),
|
||||
ThreeWayMergeForUnstructured: false,
|
||||
ServerSideApply: true,
|
||||
ExpectedActions: append(expectedActionsServerSideApply,
|
||||
"/namespaces/default/pods/forbidden:GET",
|
||||
"/namespaces/default/pods/forbidden:DELETE",
|
||||
),
|
||||
ExpectedError: "failed to delete resource forbidden:",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -444,6 +468,22 @@ func TestUpdate(t *testing.T) {
|
|||
return newResponse(http.StatusOK, &listTarget.Items[1])
|
||||
case p == "/namespaces/default/pods/squid" && m == http.MethodGet:
|
||||
return newResponse(http.StatusOK, &listTarget.Items[2])
|
||||
case p == "/namespaces/default/pods/notfound" && m == http.MethodGet:
|
||||
// Resource exists in original but will simulate not found on delete
|
||||
return newResponse(http.StatusOK, &listOriginal.Items[3])
|
||||
case p == "/namespaces/default/pods/notfound" && m == http.MethodDelete:
|
||||
// Simulate a not found during deletion; should not cause update to fail
|
||||
return newResponse(http.StatusNotFound, notFoundBody())
|
||||
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:
|
||||
}
|
||||
|
||||
|
|
@ -471,7 +511,13 @@ func TestUpdate(t *testing.T) {
|
|||
ClientUpdateOptionForceReplace(false),
|
||||
ClientUpdateOptionServerSideApply(tc.ServerSideApply, false),
|
||||
ClientUpdateOptionUpgradeClientSideFieldManager(true))
|
||||
require.NoError(t, err)
|
||||
|
||||
if tc.ExpectedError != "" {
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tc.ExpectedError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
assert.Len(t, result.Created, 1, "expected 1 resource created, got %d", len(result.Created))
|
||||
assert.Len(t, result.Updated, 2, "expected 2 resource updated, got %d", len(result.Updated))
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ func (t *parser) key(data map[string]interface{}, nestedNameLevel int) (reterr e
|
|||
_, err = t.emptyVal()
|
||||
return err
|
||||
}
|
||||
//End of key. Consume =, Get value.
|
||||
// End of key. Consume =, Get value.
|
||||
// FIXME: Get value list first
|
||||
vl, e := t.valList()
|
||||
switch e {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ runAsRoot() {
|
|||
# verifySupported checks that the os/arch combination is supported for
|
||||
# binary builds.
|
||||
verifySupported() {
|
||||
local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
|
||||
local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-loong64\nlinux-ppc64le\nlinux-s390x\nwindows-amd64\nwindows-arm64"
|
||||
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
|
||||
echo "No prebuilt binary for ${OS}-${ARCH}."
|
||||
echo "To build from source, go to https://github.com/helm/helm"
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ runAsRoot() {
|
|||
# verifySupported checks that the os/arch combination is supported for
|
||||
# binary builds, as well whether or not necessary tools are present.
|
||||
verifySupported() {
|
||||
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
|
||||
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-loong64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64\nwindows-arm64"
|
||||
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
|
||||
echo "No prebuilt binary for ${OS}-${ARCH}."
|
||||
echo "To build from source, go to https://github.com/helm/helm"
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ Download Helm ${RELEASE}. The common platform binaries are here:
|
|||
- [Linux arm](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm.tar.gz.sha256))
|
||||
- [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256))
|
||||
- [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256))
|
||||
- [Linux loong64](https://get.helm.sh/helm-${RELEASE}-linux-loong64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-loong64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-loong64.tar.gz.sha256))
|
||||
- [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256))
|
||||
- [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-s390x.tar.gz.sha256))
|
||||
- [Linux riscv64](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-riscv64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-riscv64.tar.gz.sha256))
|
||||
|
|
|
|||
Loading…
Reference in a new issue