Merge remote-tracking branch 'upstream/main' into fixup_release

Signed-off-by: George Jenkins <gvjenkins@gmail.com>
pull/12672/head
George Jenkins 6 months ago
commit 57265115d8

@ -1,18 +0,0 @@
name: Publish Release Assets to Asset Transparency Log
on:
release:
types: [published, created, edited, released]
jobs:
github_release_asset_transparency_log_publish_job:
runs-on: ubuntu-latest
name: Publish GitHub release asset digests to https://beta-asset.transparencylog.net
steps:
- name: Gather URLs from GitHub release and publish
id: asset-transparency
uses: transparencylog/github-releases-asset-transparency-verify-action@c77874b4514ae4003994ece9582675195fe012e2 # v11
- name: List verified and published URLs
run: echo "Verified URLs ${{ steps.asset-transparency.outputs.verified }}"
- name: List failed URLs
run: echo "Failed URLs ${{ steps.asset-transparency.outputs.failed }}"

@ -17,19 +17,9 @@ jobs:
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: '1.20'
- name: Install golangci-lint
run: |
curl -sSLO https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz
shasum -a 256 golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | grep "^$GOLANGCI_LINT_SHA256 " > /dev/null
tar -xf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz
sudo mv golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64/golangci-lint /usr/local/bin/golangci-lint
rm -rf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64*
env:
GOLANGCI_LINT_VERSION: '1.51.2'
GOLANGCI_LINT_SHA256: '4de479eb9d9bc29da51aec1834e7c255b333723d38dbd56781c68e5dddc6a90b'
- name: Test style
run: make test-style
go-version: '1.21'
- name: Test source headers are present
run: make test-source-headers
- name: Run unit tests
run: make test-coverage
- name: Test build

@ -0,0 +1,22 @@
name: golangci-lint
on:
push:
pull_request:
jobs:
golangci:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin@v4.1.1
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: "1.21"
- name: golangci-lint
uses: golangci/golangci-lint-action@3a919529898de77ec3da873e3063ca4b10e7f5cc #pin@3.7.0
with:
version: v1.55

@ -25,7 +25,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: '1.20'
go-version: '1.21'
- name: Run unit tests
run: make test-coverage
@ -81,7 +81,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # pin@4.1.0
with:
go-version: '1.20'
go-version: '1.21'
- name: Run unit tests
run: make test-coverage

@ -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 windows/amd64
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 windows-amd64.zip windows-amd64.zip.sha256 windows-amd64.zip.sha256sum
TARGETS := darwin/amd64 darwin/arm64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x linux/riscv64 windows/amd64
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
BINNAME ?= helm
GOBIN = $(shell go env GOBIN)
@ -114,7 +114,11 @@ test-coverage:
.PHONY: test-style
test-style:
GO111MODULE=on golangci-lint run
golangci-lint run ./...
@scripts/validate-license.sh
.PHONY: test-source-headers
test-source-headers:
@scripts/validate-license.sh
.PHONY: test-acceptance
@ -155,7 +159,7 @@ gen-test-golden: test-unit
# without a go.mod file when downloading the following dependencies
$(GOX):
(cd /; GO111MODULE=on go install github.com/mitchellh/gox@latest)
(cd /; GO111MODULE=on go install github.com/mitchellh/gox@v1.0.2-0.20220701044238-9f712387e2d2)
$(GOIMPORTS):
(cd /; GO111MODULE=on go install golang.org/x/tools/cmd/goimports@latest)

@ -210,6 +210,6 @@ func runCompletionPowershell(out io.Writer, cmd *cobra.Command) error {
}
// Function to disable file completion
func noCompletions(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
func noCompletions(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
}

@ -77,7 +77,7 @@ func newDocsCmd(out io.Writer) *cobra.Command {
return cmd
}
func (o *docsOptions) run(out io.Writer) error {
func (o *docsOptions) run(_ io.Writer) error {
switch o.docTypeString {
case "markdown", "mdown", "md":
if o.generateHeaders {

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

@ -184,7 +184,7 @@ func min(x, y int) int {
return y
}
func compListRevisions(toComplete string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) {
func compListRevisions(_ string, cfg *action.Configuration, releaseName string) ([]string, cobra.ShellCompDirective) {
client := action.NewHistory(cfg)
var revisions []string

@ -27,6 +27,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/lint/support"
@ -44,6 +45,7 @@ or recommendation, it will emit [WARNING] messages.
func newLintCmd(out io.Writer) *cobra.Command {
client := action.NewLint()
valueOpts := &values.Options{}
var kubeVersion string
cmd := &cobra.Command{
Use: "lint PATH",
@ -54,6 +56,15 @@ func newLintCmd(out io.Writer) *cobra.Command {
if len(args) > 0 {
paths = args
}
if kubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}
if client.WithSubcharts {
for _, p := range paths {
filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error {
@ -137,6 +148,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for capabilities and deprecation checks")
addValueOptionsFlags(f, valueOpts)
return cmd

@ -63,6 +63,34 @@ func TestLintCmdWithQuietFlag(t *testing.T) {
}
func TestLintCmdWithKubeVersionFlag(t *testing.T) {
testChart := "testdata/testcharts/chart-with-deprecated-api"
tests := []cmdTestCase{{
name: "lint chart with deprecated api version using kube version flag",
cmd: fmt.Sprintf("lint --kube-version 1.22.0 %s", testChart),
golden: "output/lint-chart-with-deprecated-api.txt",
wantError: false,
}, {
name: "lint chart with deprecated api version using kube version and strict flag",
cmd: fmt.Sprintf("lint --kube-version 1.22.0 --strict %s", testChart),
golden: "output/lint-chart-with-deprecated-api-strict.txt",
wantError: true,
}, {
// the test builds will use the default k8sVersionMinor const in deprecations.go and capabilities.go
// which is "20"
name: "lint chart with deprecated api version without kube version",
cmd: fmt.Sprintf("lint %s", testChart),
golden: "output/lint-chart-with-deprecated-api-old-k8s.txt",
wantError: false,
}, {
name: "lint chart with deprecated api version with older kube version",
cmd: fmt.Sprintf("lint --kube-version 1.21.0 --strict %s", testChart),
golden: "output/lint-chart-with-deprecated-api-old-k8s.txt",
wantError: false,
}}
runTestCmd(t, tests)
}
func TestLintFileCompletion(t *testing.T) {
checkFileCompletion(t, "lint", true)
checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given

@ -75,7 +75,7 @@ func filterPlugins(plugins []*plugin.Plugin, ignoredPluginNames []string) []*plu
}
// Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string, ignoredPluginNames []string) []string {
func compListPlugins(_ string, ignoredPluginNames []string) []string {
var pNames []string
plugins, err := plugin.FindPlugins(settings.PluginsDirectory)
if err == nil && len(plugins) > 0 {

@ -76,7 +76,7 @@ func newRepoIndexCmd(out io.Writer) *cobra.Command {
return cmd
}
func (i *repoIndexOptions) run(out io.Writer) error {
func (i *repoIndexOptions) run(_ io.Writer) error {
path, err := filepath.Abs(i.dir)
if err != nil {
return err

@ -123,7 +123,7 @@ func filterRepos(repos []*repo.Entry, ignoredRepoNames []string) []*repo.Entry {
}
// Provide dynamic auto-completion for repo names
func compListRepos(prefix string, ignoredRepoNames []string) []string {
func compListRepos(_ string, ignoredRepoNames []string) []string {
var rNames []string
f, err := repo.LoadFile(settings.RepositoryConfig)

@ -45,31 +45,32 @@ Common actions for Helm:
Environment variables:
| Name | Description |
|------------------------------------|---------------------------------------------------------------------------------------------------|
| $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. |
| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. |
| $HELM_PLUGINS | set the path to the plugins directory |
| $HELM_REGISTRY_CONFIG | set the path to the registry config file. |
| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory |
| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. |
| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication |
| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. |
| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. |
| $HELM_KUBEASUSER | set the Username to impersonate for the operation. |
| $HELM_KUBECONTEXT | set the name of the kubeconfig context. |
| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. |
| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) |
| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate |
| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable)|
| Name | Description |
|------------------------------------|------------------------------------------------------------------------------------------------------------|
| $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. |
| $HELM_DEBUG | indicate whether or not Helm is running in Debug mode |
| $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, sql. |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. |
| $HELM_MAX_HISTORY | set the maximum number of helm release history. |
| $HELM_NAMESPACE | set the namespace used for the helm operations. |
| $HELM_NO_PLUGINS | disable plugins. Set HELM_NO_PLUGINS=1 to disable plugins. |
| $HELM_PLUGINS | set the path to the plugins directory |
| $HELM_REGISTRY_CONFIG | set the path to the registry config file. |
| $HELM_REPOSITORY_CACHE | set the path to the repository cache directory |
| $HELM_REPOSITORY_CONFIG | set the path to the repositories file. |
| $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication |
| $HELM_KUBECAFILE | set the Kubernetes certificate authority file. |
| $HELM_KUBEASGROUPS | set the Groups to use for impersonation using a comma-separated list. |
| $HELM_KUBEASUSER | set the Username to impersonate for the operation. |
| $HELM_KUBECONTEXT | set the name of the kubeconfig context. |
| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. |
| $HELM_KUBEINSECURE_SKIP_TLS_VERIFY | indicate if the Kubernetes API server's certificate validation should be skipped (insecure) |
| $HELM_KUBETLS_SERVER_NAME | set the server name used to validate the Kubernetes API server certificate |
| $HELM_BURST_LIMIT | set the default burst limit in the case the server contains many CRDs (default 100, -1 to disable) |
| $HELM_QPS | set the Queries Per Second in cases where a high number of calls exceed the option for higher burst values |
Helm stores cache, configuration, and data based on the following configuration order:

@ -101,7 +101,7 @@ var indexfileEntries = map[string]repo.ChartVersions{
},
}
func loadTestIndex(t *testing.T, all bool) *Index {
func loadTestIndex(_ *testing.T, all bool) *Index {
i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{

@ -15,6 +15,7 @@ HELM_KUBETOKEN
HELM_MAX_HISTORY
HELM_NAMESPACE
HELM_PLUGINS
HELM_QPS
HELM_REGISTRY_CONFIG
HELM_REPOSITORY_CACHE
HELM_REPOSITORY_CONFIG

@ -0,0 +1,4 @@
==> Linting testdata/testcharts/chart-with-deprecated-api
[INFO] Chart.yaml: icon is recommended
1 chart(s) linted, 0 chart(s) failed

@ -0,0 +1,5 @@
==> Linting testdata/testcharts/chart-with-deprecated-api
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler
Error: 1 chart(s) linted, 1 chart(s) failed

@ -0,0 +1,5 @@
==> Linting testdata/testcharts/chart-with-deprecated-api
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/horizontalpodautoscaler.yaml: autoscaling/v2beta1 HorizontalPodAutoscaler is deprecated in v1.22+, unavailable in v1.25+; use autoscaling/v2 HorizontalPodAutoscaler
1 chart(s) linted, 0 chart(s) failed

@ -0,0 +1,6 @@
apiVersion: v2
appVersion: "1.0.0"
description: A Helm chart for Kubernetes
name: chart-with-deprecated-api
type: application
version: 1.0.0

@ -0,0 +1,9 @@
apiVersion: autoscaling/v2beta1
kind: HorizontalPodAutoscaler
metadata:
name: deprecated
spec:
scaleTargetRef:
kind: Pod
name: pod
maxReplicas: 3

@ -1,6 +1,6 @@
module helm.sh/helm/v3
go 1.20
go 1.21
require (
github.com/BurntSushi/toml v1.3.2
@ -10,7 +10,7 @@ require (
github.com/Masterminds/squirrel v1.5.4
github.com/Masterminds/vcs v1.13.3
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535
github.com/containerd/containerd v1.7.6
github.com/containerd/containerd v1.7.11
github.com/cyphar/filepath-securejoin v0.2.4
github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2
github.com/evanphx/json-patch v5.7.0+incompatible
@ -29,13 +29,13 @@ require (
github.com/pkg/errors v0.9.1
github.com/rubenv/sql-migrate v1.5.2
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.7.0
github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.4
github.com/xeipuuv/gojsonschema v1.2.0
golang.org/x/crypto v0.14.0
golang.org/x/term v0.13.0
golang.org/x/text v0.13.0
golang.org/x/crypto v0.17.0
golang.org/x/term v0.15.0
golang.org/x/text v0.14.0
k8s.io/api v0.29.0
k8s.io/apiextensions-apiserver v0.29.0
k8s.io/apimachinery v0.29.0
@ -53,7 +53,7 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Microsoft/hcsshim v0.11.0 // indirect
github.com/Microsoft/hcsshim v0.11.4 // indirect
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bshuster-repo/logrus-logstash-hook v1.0.0 // indirect
@ -62,7 +62,8 @@ require (
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/chai2010/gettext-go v1.0.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/cli v24.0.6+incompatible // indirect
github.com/docker/distribution v2.8.2+incompatible // indirect
@ -141,6 +142,7 @@ require (
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 // indirect
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 // indirect
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
@ -148,7 +150,7 @@ require (
golang.org/x/net v0.17.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect

@ -23,8 +23,8 @@ github.com/Masterminds/vcs v1.13.3 h1:IIA2aBdXvfbIM+yl/eTnL4hb1XwdpvuQLglAix1gwe
github.com/Masterminds/vcs v1.13.3/go.mod h1:TiE7xuEjl1N4j016moRd6vezp6e6Lz23gypeXfzXeW8=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/Microsoft/hcsshim v0.11.0 h1:7EFNIY4igHEXUdj1zXgAyU3fLc7QfOKHbkldRVTBdiM=
github.com/Microsoft/hcsshim v0.11.0/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM=
github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8=
github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -58,12 +58,14 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
github.com/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8=
github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4=
github.com/containerd/containerd v1.7.11 h1:lfGKw3eU35sjV0aG2eYZTiwFEY1pCzxdzicHP3SZILw=
github.com/containerd/containerd v1.7.11/go.mod h1:5UluHxHTX2rdvYuZ5OJTC5m/KJNs0Zs9wVoJm9zf5ZE=
github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM=
github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
@ -365,8 +367,8 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -403,6 +405,8 @@ github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q=
go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs=
go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY=
go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE=
@ -418,8 +422,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
@ -478,21 +482,21 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

@ -22,6 +22,7 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/pkg/kube"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
@ -51,7 +52,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
h.DeletePolicies = []release.HookDeletePolicy{release.HookBeforeHookCreation}
}
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookBeforeHookCreation, timeout); err != nil {
return err
}
@ -88,7 +89,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
h.LastRun.Phase = release.HookPhaseFailed
// If a hook is failed, check the annotation of the hook to determine whether the hook should be deleted
// under failed condition. If so, then clear the corresponding resource object in the hook
if err := cfg.deleteHookByPolicy(h, release.HookFailed); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookFailed, timeout); err != nil {
return err
}
return err
@ -99,7 +100,7 @@ func (cfg *Configuration) execHook(rl *release.Release, hook release.HookEvent,
// If all hooks are successful, check the annotation of each hook to determine whether the hook should be deleted
// under succeeded condition. If so, then clear the corresponding resource object in each hook
for _, h := range executingHooks {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded); err != nil {
if err := cfg.deleteHookByPolicy(h, release.HookSucceeded, timeout); err != nil {
return err
}
}
@ -120,7 +121,7 @@ func (x hookByWeight) Less(i, j int) bool {
}
// deleteHookByPolicy deletes a hook if the hook policy instructs it to
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy) error {
func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.HookDeletePolicy, timeout time.Duration) error {
// Never delete CustomResourceDefinitions; this could cause lots of
// cascading garbage collection.
if h.Kind == "CustomResourceDefinition" {
@ -135,6 +136,13 @@ func (cfg *Configuration) deleteHookByPolicy(h *release.Hook, policy release.Hoo
if len(errs) > 0 {
return errors.New(joinErrors(errs))
}
//wait for resources until they are deleted to avoid conflicts
if kubeClient, ok := cfg.KubeClient.(kube.InterfaceExt); ok {
if err := kubeClient.WaitForDelete(resources, timeout); err != nil {
return err
}
}
}
return nil
}

@ -431,6 +431,9 @@ func TestInstallRelease_Atomic(t *testing.T) {
failer.WaitError = fmt.Errorf("I timed out")
instAction.cfg.KubeClient = failer
instAction.Atomic = true
// disabling hooks to avoid an early fail when the
// the WaitForDelete is called on the pre-delete hook execution
instAction.DisableHooks = true
vals := map[string]interface{}{}
res, err := instAction.Run(buildChart(), vals)

@ -36,6 +36,7 @@ type Lint struct {
Namespace string
WithSubcharts bool
Quiet bool
KubeVersion *chartutil.KubeVersion
}
// LintResult is the result of Lint
@ -58,7 +59,7 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
}
result := &LintResult{}
for _, path := range paths {
linter, err := lintChart(path, vals, l.Namespace, l.Strict)
linter, err := lintChart(path, vals, l.Namespace, l.KubeVersion)
if err != nil {
result.Errors = append(result.Errors, err)
continue
@ -85,7 +86,7 @@ func HasWarningsOrErrors(result *LintResult) bool {
return len(result.Errors) > 0
}
func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) {
func lintChart(path string, vals map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) (support.Linter, error) {
var chartPath string
linter := support.Linter{}
@ -124,5 +125,5 @@ func lintChart(path string, vals map[string]interface{}, namespace string, stric
return linter, errors.Wrap(err, "unable to check Chart.yaml file in chart")
}
return lint.All(chartPath, vals, namespace, strict), nil
return lint.AllWithKubeVersion(chartPath, vals, namespace, kubeVersion), nil
}

@ -23,7 +23,6 @@ import (
var (
values = make(map[string]interface{})
namespace = "testNamespace"
strict = false
chart1MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-1"
chart2MultipleChartLint = "testdata/charts/multiplecharts-lint-chart-2"
corruptedTgzChart = "testdata/charts/corrupted-compressed-chart.tgz"
@ -78,7 +77,7 @@ func TestLintChart(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, strict)
_, err := lintChart(tt.chartPath, map[string]interface{}{}, namespace, nil)
switch {
case err != nil && !tt.err:
t.Errorf("%s", err)

@ -54,7 +54,7 @@ func NewPackage() *Package {
}
// Run executes 'helm package' against the given chart and returns the path to the packaged chart.
func (p *Package) Run(path string, vals map[string]interface{}) (string, error) {
func (p *Package) Run(path string, _ map[string]interface{}) (string, error) {
ch, err := loader.LoadDir(path)
if err != nil {
return "", err

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

@ -33,6 +33,6 @@ func NewRegistryLogout(cfg *Configuration) *RegistryLogout {
}
// Run executes the registry logout operation
func (a *RegistryLogout) Run(out io.Writer, hostname string) error {
func (a *RegistryLogout) Run(_ io.Writer, hostname string) error {
return a.cfg.RegistryClient.Logout(hostname)
}

@ -25,9 +25,9 @@ import (
"github.com/pkg/errors"
"helm.sh/helm/v3/internal/ignore"
"helm.sh/helm/v3/internal/sympath"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/ignore"
)
var utf8bom = []byte{0xEF, 0xBB, 0xBF}

@ -128,10 +128,19 @@ func (md *Metadata) Validate() error {
// Aliases need to be validated here to make sure that the alias name does
// not contain any illegal characters.
dependencies := map[string]*Dependency{}
for _, dependency := range md.Dependencies {
if err := dependency.Validate(); err != nil {
return err
}
key := dependency.Name
if dependency.Alias != "" {
key = dependency.Alias
}
if dependencies[key] != nil {
return ValidationErrorf("more than one dependency with name or alias %q", key)
}
dependencies[key] = dependency
}
return nil
}

@ -21,34 +21,42 @@ import (
func TestValidate(t *testing.T) {
tests := []struct {
md *Metadata
err error
name string
md *Metadata
err error
}{
{
"chart without metadata",
nil,
ValidationError("chart.metadata is required"),
},
{
"chart without apiVersion",
&Metadata{Name: "test", Version: "1.0"},
ValidationError("chart.metadata.apiVersion is required"),
},
{
"chart without name",
&Metadata{APIVersion: "v2", Version: "1.0"},
ValidationError("chart.metadata.name is required"),
},
{
"chart without version",
&Metadata{Name: "test", APIVersion: "v2"},
ValidationError("chart.metadata.version is required"),
},
{
"chart with bad type",
&Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "test"},
ValidationError("chart.metadata.type must be application or library"),
},
{
"chart without dependency",
&Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"},
nil,
},
{
"dependency with valid alias",
&Metadata{
Name: "test",
APIVersion: "v2",
@ -61,6 +69,7 @@ func TestValidate(t *testing.T) {
nil,
},
{
"dependency with bad characters in alias",
&Metadata{
Name: "test",
APIVersion: "v2",
@ -73,6 +82,67 @@ func TestValidate(t *testing.T) {
ValidationError("dependency \"bad\" has disallowed characters in the alias"),
},
{
"same dependency twice",
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
{Name: "foo", Alias: ""},
{Name: "foo", Alias: ""},
},
},
ValidationError("more than one dependency with name or alias \"foo\""),
},
{
"two dependencies with alias from second dependency shadowing first one",
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
{Name: "foo", Alias: ""},
{Name: "bar", Alias: "foo"},
},
},
ValidationError("more than one dependency with name or alias \"foo\""),
},
{
// this case would make sense and could work in future versions of Helm, currently template rendering would
// result in undefined behaviour
"same dependency twice with different version",
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
{Name: "foo", Alias: "", Version: "1.2.3"},
{Name: "foo", Alias: "", Version: "1.0.0"},
},
},
ValidationError("more than one dependency with name or alias \"foo\""),
},
{
// this case would make sense and could work in future versions of Helm, currently template rendering would
// result in undefined behaviour
"two dependencies with same name but different repos",
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
{Name: "foo", Repository: "repo-0"},
{Name: "foo", Repository: "repo-1"},
},
},
ValidationError("more than one dependency with name or alias \"foo\""),
},
{
"dependencies has nil",
&Metadata{
Name: "test",
APIVersion: "v2",
@ -85,6 +155,7 @@ func TestValidate(t *testing.T) {
ValidationError("dependencies must not contain empty or null nodes"),
},
{
"maintainer not empty",
&Metadata{
Name: "test",
APIVersion: "v2",
@ -97,6 +168,7 @@ func TestValidate(t *testing.T) {
ValidationError("maintainers must not contain empty or null nodes"),
},
{
"version invalid",
&Metadata{APIVersion: "v2", Name: "test", Version: "1.2.3.4"},
ValidationError("chart.metadata.version \"1.2.3.4\" is invalid"),
},
@ -105,7 +177,7 @@ func TestValidate(t *testing.T) {
for _, tt := range tests {
result := tt.md.Validate()
if result != tt.err {
t.Errorf("expected '%s', got '%s'", tt.err, result)
t.Errorf("expected %q, got %q in test %q", tt.err, result, tt.name)
}
}
}

@ -129,7 +129,7 @@ func coalesceDeps(printf printFn, chrt *chart.Chart, dest map[string]interface{}
// coalesceGlobals copies the globals out of src and merges them into dest.
//
// For convenience, returns dest.
func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, merge bool) {
func coalesceGlobals(printf printFn, dest, src map[string]interface{}, prefix string, _ bool) {
var dg, sg map[string]interface{}
if destglob, ok := dest[GlobalKey]; !ok {

@ -175,6 +175,15 @@ resources: {}
# cpu: 100m
# memory: 128Mi
livenessProbe:
httpGet:
path: /
port: http
readinessProbe:
httpGet:
path: /
port: http
autoscaling:
enabled: false
minReplicas: 1
@ -333,13 +342,9 @@ spec:
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /
port: http
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
httpGet:
path: /
port: http
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}

@ -59,9 +59,8 @@ func processDependencyConditions(reqs []*chart.Dependency, cvals Values, cpath s
if bv, ok := vv.(bool); ok {
r.Enabled = bv
break
} else {
log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
}
log.Printf("Warning: Condition path '%s' for chart %s returned non-bool value", c, r.Name)
} else if _, ok := err.(ErrNoValue); !ok {
// this is a real error
log.Printf("Warning: PathValue returned error %v", err)

@ -44,6 +44,9 @@ const defaultMaxHistory = 10
// defaultBurstLimit sets the default client-side throttling limit
const defaultBurstLimit = 100
// defaultQPS sets the default QPS value to 0 to to use library defaults unless specified
const defaultQPS = float32(0)
// EnvSettings describes all of the environment settings.
type EnvSettings struct {
namespace string
@ -83,6 +86,8 @@ type EnvSettings struct {
MaxHistory int
// BurstLimit is the default client-side throttling limit.
BurstLimit int
// QPS is queries per second which may be used to avoid throttling.
QPS float32
}
func New() *EnvSettings {
@ -102,6 +107,7 @@ func New() *EnvSettings {
RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")),
RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")),
BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit),
QPS: envFloat32Or("HELM_QPS", defaultQPS),
}
env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG"))
@ -119,6 +125,7 @@ func New() *EnvSettings {
ImpersonateGroup: &env.KubeAsGroups,
WrapConfigFn: func(config *rest.Config) *rest.Config {
config.Burst = env.BurstLimit
config.QPS = env.QPS
config.Wrap(func(rt http.RoundTripper) http.RoundTripper {
return &retryingRoundTripper{wrapped: rt}
})
@ -146,6 +153,7 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs")
fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes")
fs.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "client-side default throttling limit")
fs.Float32Var(&s.QPS, "qps", s.QPS, "queries per second used when communicating with the Kubernetes API, not including bursting")
}
func envOr(name, def string) string {
@ -179,6 +187,18 @@ func envIntOr(name string, def int) int {
return ret
}
func envFloat32Or(name string, def float32) float32 {
if name == "" {
return def
}
envVal := envOr(name, strconv.FormatFloat(float64(def), 'f', 2, 32))
ret, err := strconv.ParseFloat(envVal, 32)
if err != nil {
return def
}
return float32(ret)
}
func envCSV(name string) (ls []string) {
trimmed := strings.Trim(os.Getenv(name), ", ")
if trimmed != "" {
@ -201,6 +221,7 @@ func (s *EnvSettings) EnvVars() map[string]string {
"HELM_NAMESPACE": s.Namespace(),
"HELM_MAX_HISTORY": strconv.Itoa(s.MaxHistory),
"HELM_BURST_LIMIT": strconv.Itoa(s.BurstLimit),
"HELM_QPS": strconv.FormatFloat(float64(s.QPS), 'f', 2, 32),
// broken, these are populated from helm flags and not kubeconfig.
"HELM_KUBECONTEXT": s.KubeContext,

@ -59,20 +59,23 @@ func TestEnvSettings(t *testing.T) {
kubeInsecure bool
kubeTLSServer string
burstLimit int
qps float32
}{
{
name: "defaults",
ns: "default",
maxhistory: defaultMaxHistory,
burstLimit: defaultBurstLimit,
qps: defaultQPS,
},
{
name: "with flags set",
args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/tmp/ca.crt --burst-limit 100 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org",
args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/tmp/ca.crt --burst-limit 100 --qps 50.12 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org",
ns: "myns",
debug: true,
maxhistory: defaultMaxHistory,
burstLimit: 100,
qps: 50.12,
kubeAsUser: "poro",
kubeAsGroups: []string{"admins", "teatime", "snackeaters"},
kubeCaFile: "/tmp/ca.crt",
@ -81,10 +84,11 @@ func TestEnvSettings(t *testing.T) {
},
{
name: "with envvars set",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "150", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org"},
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "150", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org", "HELM_QPS": "60.34"},
ns: "yourns",
maxhistory: 5,
burstLimit: 150,
qps: 60.34,
debug: true,
kubeAsUser: "pikachu",
kubeAsGroups: []string{"operators", "snackeaters", "partyanimals"},
@ -94,12 +98,13 @@ func TestEnvSettings(t *testing.T) {
},
{
name: "with flags and envvars set",
args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt --burst-limit 175 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "200", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org"},
args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt --burst-limit 175 --qps 70 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "200", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org", "HELM_QPS": "40"},
ns: "myns",
debug: true,
maxhistory: 5,
burstLimit: 175,
qps: 70,
kubeAsUser: "poro",
kubeAsGroups: []string{"admins", "teatime", "snackeaters"},
kubeCaFile: "/my/ca.crt",

@ -714,15 +714,21 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
var entry repo.ChartVersions
entry, err = findEntryByName(name, cr)
if err != nil {
// TODO: Where linting is skipped in this function we should
// refactor to remove naked returns while ensuring the same
// behavior
//nolint:nakedret
return
}
var ve *repo.ChartVersion
ve, err = findVersionedEntry(version, entry)
if err != nil {
//nolint:nakedret
return
}
url, err = normalizeURL(repoURL, ve.URLs[0])
if err != nil {
//nolint:nakedret
return
}
username = cr.Config.Username
@ -732,6 +738,7 @@ func (m *Manager) findChartURL(name, version, repoURL string, repos map[string]*
caFile = cr.Config.CAFile
certFile = cr.Config.CertFile
keyFile = cr.Config.KeyFile
//nolint:nakedret
return
}
}

@ -40,16 +40,17 @@ type Engine struct {
Strict bool
// In LintMode, some 'required' template values may be missing, so don't fail
LintMode bool
// the rest config to connect to the kubernetes api
config *rest.Config
// optional provider of clients to talk to the Kubernetes API
clientProvider *ClientProvider
// EnableDNS tells the engine to allow DNS lookups when rendering templates
EnableDNS bool
}
// New creates a new instance of Engine using the passed in rest config.
func New(config *rest.Config) Engine {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
config: config,
clientProvider: &clientProvider,
}
}
@ -85,10 +86,21 @@ func Render(chrt *chart.Chart, values chartutil.Values) (map[string]string, erro
// RenderWithClient takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client
// functions that interact with the client.
func RenderWithClient(chrt *chart.Chart, values chartutil.Values, config *rest.Config) (map[string]string, error) {
var clientProvider ClientProvider = clientProviderFromConfig{config}
return Engine{
config: config,
clientProvider: &clientProvider,
}.Render(chrt, values)
}
// RenderWithClientProvider takes a chart, optional values, and value overrides, and attempts to
// render the Go templates using the default options. This engine is client aware and so can have template
// functions that interact with the client.
// This function differs from RenderWithClient in that it lets you customize the way a dynamic client is constructed.
func RenderWithClientProvider(chrt *chart.Chart, values chartutil.Values, clientProvider ClientProvider) (map[string]string, error) {
return Engine{
clientProvider: &clientProvider,
}.Render(chrt, values)
}
@ -112,13 +124,10 @@ func warnWrap(warn string) string {
return warnStartDelim + warn + warnEndDelim
}
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]renderable) {
funcMap := funcMap()
includedNames := make(map[string]int)
// Add the 'include' function here so we can close over t.
funcMap["include"] = func(name string, data interface{}) (string, error) {
// 'include' needs to be defined in the scope of a 'tpl' template as
// well as regular file-loaded templates.
func includeFun(t *template.Template, includedNames map[string]int) func(string, interface{}) (string, error) {
return func(name string, data interface{}) (string, error) {
var buf strings.Builder
if v, ok := includedNames[name]; ok {
if v > recursionMaxNums {
@ -132,33 +141,62 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
includedNames[name]--
return buf.String(), err
}
}
// Add the 'tpl' function here
funcMap["tpl"] = func(tpl string, vals chartutil.Values) (string, error) {
basePath, err := vals.PathValue("Template.BasePath")
// As does 'tpl', so that nested calls to 'tpl' see the templates
// defined by their enclosing contexts.
func tplFun(parent *template.Template, includedNames map[string]int, strict bool) func(string, interface{}) (string, error) {
return func(tpl string, vals interface{}) (string, error) {
t, err := parent.Clone()
if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Basepath from values inside tpl function: %s", tpl)
return "", errors.Wrapf(err, "cannot clone template")
}
templateName, err := vals.PathValue("Template.Name")
if err != nil {
return "", errors.Wrapf(err, "cannot retrieve Template.Name from values inside tpl function: %s", tpl)
// Re-inject the missingkey option, see text/template issue https://github.com/golang/go/issues/43022
// We have to go by strict from our engine configuration, as the option fields are private in Template.
// TODO: Remove workaround (and the strict parameter) once we build only with golang versions with a fix.
if strict {
t.Option("missingkey=error")
} else {
t.Option("missingkey=zero")
}
templates := map[string]renderable{
templateName.(string): {
tpl: tpl,
vals: vals,
basePath: basePath.(string),
},
// Re-inject 'include' so that it can close over our clone of t;
// this lets any 'define's inside tpl be 'include'd.
t.Funcs(template.FuncMap{
"include": includeFun(t, includedNames),
"tpl": tplFun(t, includedNames, strict),
})
// We need a .New template, as template text which is just blanks
// or comments after parsing out defines just addes new named
// template definitions without changing the main template.
// https://pkg.go.dev/text/template#Template.Parse
// Use the parent's name for lack of a better way to identify the tpl
// text string. (Maybe we could use a hash appended to the name?)
t, err = t.New(parent.Name()).Parse(tpl)
if err != nil {
return "", errors.Wrapf(err, "cannot parse template %q", tpl)
}
result, err := e.renderWithReferences(templates, referenceTpls)
if err != nil {
var buf strings.Builder
if err := t.Execute(&buf, vals); err != nil {
return "", errors.Wrapf(err, "error during tpl function execution for %q", tpl)
}
return result[templateName.(string)], nil
// See comment in renderWithReferences explaining the <no value> hack.
return strings.ReplaceAll(buf.String(), "<no value>", ""), nil
}
}
// initFunMap creates the Engine's FuncMap and adds context-specific functions.
func (e Engine) initFunMap(t *template.Template) {
funcMap := funcMap()
includedNames := make(map[string]int)
// Add the template-rendering functions here so we can close over t.
funcMap["include"] = includeFun(t, includedNames)
funcMap["tpl"] = tplFun(t, includedNames, e.Strict)
// Add the `required` function here so we can use lintMode
funcMap["required"] = func(warn string, val interface{}) (interface{}, error) {
@ -194,8 +232,8 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
// If we are not linting and have a cluster connection, provide a Kubernetes-backed
// implementation.
if !e.LintMode && e.config != nil {
funcMap["lookup"] = NewLookupFunction(e.config)
if !e.LintMode && e.clientProvider != nil {
funcMap["lookup"] = newLookupFunction(*e.clientProvider)
}
// When DNS lookups are not enabled override the sprig function and return
@ -210,13 +248,7 @@ func (e Engine) initFunMap(t *template.Template, referenceTpls map[string]render
}
// render takes a map of templates/values and renders them.
func (e Engine) render(tpls map[string]renderable) (map[string]string, error) {
return e.renderWithReferences(tpls, tpls)
}
// renderWithReferences takes a map of templates/values to render, and a map of
// templates which can be referenced within them.
func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable) (rendered map[string]string, err error) {
func (e Engine) render(tpls map[string]renderable) (rendered map[string]string, err error) {
// Basically, what we do here is start with an empty parent template and then
// build up a list of templates -- one for each file. Once all of the templates
// have been parsed, we loop through again and execute every template.
@ -238,12 +270,11 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
t.Option("missingkey=zero")
}
e.initFunMap(t, referenceTpls)
e.initFunMap(t)
// We want to parse the templates in a predictable order. The order favors
// higher-level (in file system) templates over deeply nested templates.
keys := sortTemplates(tpls)
referenceKeys := sortTemplates(referenceTpls)
for _, filename := range keys {
r := tpls[filename]
@ -252,17 +283,6 @@ func (e Engine) renderWithReferences(tpls, referenceTpls map[string]renderable)
}
}
// Adding the reference templates to the template context
// so they can be referenced in the tpl function
for _, filename := range referenceKeys {
if t.Lookup(filename) == nil {
r := referenceTpls[filename]
if _, err := t.New(filename).Parse(r.tpl); err != nil {
return map[string]string{}, cleanupParseError(filename, err)
}
}
}
rendered = make(map[string]string, len(keys))
for _, filename := range keys {
// Don't render partials. We don't care out the direct output of partials.

@ -24,6 +24,12 @@ import (
"testing"
"text/template"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/dynamic/fake"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
)
@ -204,7 +210,7 @@ func TestRenderInternals(t *testing.T) {
}
}
func TestRenderWIthDNS(t *testing.T) {
func TestRenderWithDNS(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
@ -240,6 +246,178 @@ func TestRenderWIthDNS(t *testing.T) {
}
}
type kindProps struct {
shouldErr error
gvr schema.GroupVersionResource
namespaced bool
}
type testClientProvider struct {
t *testing.T
scheme map[string]kindProps
objects []runtime.Object
}
func (p *testClientProvider) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
props := p.scheme[path.Join(apiVersion, kind)]
if props.shouldErr != nil {
return nil, false, props.shouldErr
}
return fake.NewSimpleDynamicClient(runtime.NewScheme(), p.objects...).Resource(props.gvr), props.namespaced, nil
}
var _ ClientProvider = &testClientProvider{}
// makeUnstructured is a convenience function for single-line creation of Unstructured objects.
func makeUnstructured(apiVersion, kind, name, namespace string) *unstructured.Unstructured {
ret := &unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": apiVersion,
"kind": kind,
"metadata": map[string]interface{}{
"name": name,
},
}}
if namespace != "" {
ret.Object["metadata"].(map[string]interface{})["namespace"] = namespace
}
return ret
}
func TestRenderWithClientProvider(t *testing.T) {
provider := &testClientProvider{
t: t,
scheme: map[string]kindProps{
"v1/Namespace": {
gvr: schema.GroupVersionResource{
Version: "v1",
Resource: "namespaces",
},
},
"v1/Pod": {
gvr: schema.GroupVersionResource{
Version: "v1",
Resource: "pods",
},
namespaced: true,
},
},
objects: []runtime.Object{
makeUnstructured("v1", "Namespace", "default", ""),
makeUnstructured("v1", "Pod", "pod1", "default"),
makeUnstructured("v1", "Pod", "pod2", "ns1"),
makeUnstructured("v1", "Pod", "pod3", "ns1"),
},
}
type testCase struct {
template string
output string
}
cases := map[string]testCase{
"ns-single": {
template: `{{ (lookup "v1" "Namespace" "" "default").metadata.name }}`,
output: "default",
},
"ns-list": {
template: `{{ (lookup "v1" "Namespace" "" "").items | len }}`,
output: "1",
},
"ns-missing": {
template: `{{ (lookup "v1" "Namespace" "" "absent") }}`,
output: "map[]",
},
"pod-single": {
template: `{{ (lookup "v1" "Pod" "default" "pod1").metadata.name }}`,
output: "pod1",
},
"pod-list": {
template: `{{ (lookup "v1" "Pod" "ns1" "").items | len }}`,
output: "2",
},
"pod-all": {
template: `{{ (lookup "v1" "Pod" "" "").items | len }}`,
output: "3",
},
"pod-missing": {
template: `{{ (lookup "v1" "Pod" "" "ns2") }}`,
output: "map[]",
},
}
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Values: map[string]interface{}{},
}
for name, exp := range cases {
c.Templates = append(c.Templates, &chart.File{
Name: path.Join("templates", name),
Data: []byte(exp.template),
})
}
vals := map[string]interface{}{
"Values": map[string]interface{}{},
}
v, err := chartutil.CoalesceValues(c, vals)
if err != nil {
t.Fatalf("Failed to coalesce values: %s", err)
}
out, err := RenderWithClientProvider(c, v, provider)
if err != nil {
t.Errorf("Failed to render templates: %s", err)
}
for name, want := range cases {
t.Run(name, func(t *testing.T) {
key := path.Join("moby/templates", name)
if out[key] != want.output {
t.Errorf("Expected %q, got %q", want, out[key])
}
})
}
}
func TestRenderWithClientProvider_error(t *testing.T) {
c := &chart.Chart{
Metadata: &chart.Metadata{
Name: "moby",
Version: "1.2.3",
},
Templates: []*chart.File{
{Name: "templates/error", Data: []byte(`{{ lookup "v1" "Error" "" "" }}`)},
},
Values: map[string]interface{}{},
}
vals := map[string]interface{}{
"Values": map[string]interface{}{},
}
v, err := chartutil.CoalesceValues(c, vals)
if err != nil {
t.Fatalf("Failed to coalesce values: %s", err)
}
provider := &testClientProvider{
t: t,
scheme: map[string]kindProps{
"v1/Error": {
shouldErr: fmt.Errorf("kaboom"),
},
},
}
_, err = RenderWithClientProvider(c, v, provider)
if err == nil || !strings.Contains(err.Error(), "kaboom") {
t.Errorf("Expected error from client provider when rendering, got %q", err)
}
}
func TestParallelRenderInternals(t *testing.T) {
// Make sure that we can use one Engine to run parallel template renders.
e := new(Engine)
@ -948,8 +1126,6 @@ func TestRenderTplTemplateNames(t *testing.T) {
{Name: "templates/default-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .}}`)},
{Name: "templates/modified-basepath", Data: []byte(`{{tpl "{{ .Template.BasePath }}" .Values.dot}}`)},
{Name: "templates/modified-name", Data: []byte(`{{tpl "{{ .Template.Name }}" .Values.dot}}`)},
// Current implementation injects the 'tpl' template as if it were a template file, and
// so only BasePath and Name make it through.
{Name: "templates/modified-field", Data: []byte(`{{tpl "{{ .Template.Field }}" .Values.dot}}`)},
},
}
@ -979,7 +1155,7 @@ func TestRenderTplTemplateNames(t *testing.T) {
"TplTemplateNames/templates/default-name": "TplTemplateNames/templates/default-name",
"TplTemplateNames/templates/modified-basepath": "path/to/template",
"TplTemplateNames/templates/modified-name": "name-of-template",
"TplTemplateNames/templates/modified-field": "",
"TplTemplateNames/templates/modified-field": "extra-field",
}
for file, expect := range expects {
if out[file] != expect {
@ -1001,13 +1177,17 @@ func TestRenderTplRedefines(t *testing.T) {
`{{define "manifest"}}original-in-manifest{{end}}` +
`before: {{include "manifest" .}}\n{{tpl .Values.manifestText .}}\nafter: {{include "manifest" .}}`,
)},
// The current implementation replaces the manifest text and re-parses, so a
// partial template defined only in the manifest invoking tpl cannot be accessed
// by that tpl call.
//{Name: "templates/manifest-only", Data: []byte(
// `{{define "manifest-only"}}only-in-manifest{{end}}` +
// `before: {{include "manifest-only" .}}\n{{tpl .Values.manifestOnlyText .}}\nafter: {{include "manifest-only" .}}`,
//)},
{Name: "templates/manifest-only", Data: []byte(
`{{define "manifest-only"}}only-in-manifest{{end}}` +
`before: {{include "manifest-only" .}}\n{{tpl .Values.manifestOnlyText .}}\nafter: {{include "manifest-only" .}}`,
)},
{Name: "templates/nested", Data: []byte(
`{{define "nested"}}original-in-manifest{{end}}` +
`{{define "nested-outer"}}original-outer-in-manifest{{end}}` +
`before: {{include "nested" .}} {{include "nested-outer" .}}\n` +
`{{tpl .Values.nestedText .}}\n` +
`after: {{include "nested" .}} {{include "nested-outer" .}}`,
)},
},
}
v := chartutil.Values{
@ -1015,6 +1195,12 @@ func TestRenderTplRedefines(t *testing.T) {
"partialText": `{{define "partial"}}redefined-in-tpl{{end}}tpl: {{include "partial" .}}`,
"manifestText": `{{define "manifest"}}redefined-in-tpl{{end}}tpl: {{include "manifest" .}}`,
"manifestOnlyText": `tpl: {{include "manifest-only" .}}`,
"nestedText": `{{define "nested"}}redefined-in-tpl{{end}}` +
`{{define "nested-outer"}}redefined-outer-in-tpl{{end}}` +
`before-inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}\n` +
`{{tpl .Values.innerText .}}\n` +
`after-inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`,
"innerText": `{{define "nested"}}redefined-in-inner-tpl{{end}}inner-tpl: {{include "nested" .}} {{include "nested-outer" . }}`,
},
"Chart": c.Metadata,
"Release": chartutil.Values{
@ -1028,9 +1214,14 @@ func TestRenderTplRedefines(t *testing.T) {
}
expects := map[string]string{
"TplRedefines/templates/partial": `before: original-in-partial\ntpl: original-in-partial\nafter: original-in-partial`,
"TplRedefines/templates/manifest": `before: original-in-manifest\ntpl: redefined-in-tpl\nafter: original-in-manifest`,
//"TplRedefines/templates/manifest-only": `before: only-in-manifest\ntpl: only-in-manifest\nafter: only-in-manifest`,
"TplRedefines/templates/partial": `before: original-in-partial\ntpl: redefined-in-tpl\nafter: original-in-partial`,
"TplRedefines/templates/manifest": `before: original-in-manifest\ntpl: redefined-in-tpl\nafter: original-in-manifest`,
"TplRedefines/templates/manifest-only": `before: only-in-manifest\ntpl: only-in-manifest\nafter: only-in-manifest`,
"TplRedefines/templates/nested": `before: original-in-manifest original-outer-in-manifest\n` +
`before-inner-tpl: redefined-in-tpl redefined-outer-in-tpl\n` +
`inner-tpl: redefined-in-inner-tpl redefined-outer-in-tpl\n` +
`after-inner-tpl: redefined-in-tpl redefined-outer-in-tpl\n` +
`after: original-in-manifest original-outer-in-manifest`,
}
for file, expect := range expects {
if out[file] != expect {

@ -39,9 +39,28 @@ type lookupFunc = func(apiversion string, resource string, namespace string, nam
// This function is considered deprecated, and will be renamed in Helm 4. It will no
// longer be a public function.
func NewLookupFunction(config *rest.Config) lookupFunc {
return func(apiversion string, resource string, namespace string, name string) (map[string]interface{}, error) {
return newLookupFunction(clientProviderFromConfig{config: config})
}
type ClientProvider interface {
// GetClientFor returns a dynamic.NamespaceableResourceInterface suitable for interacting with resources
// corresponding to the provided apiVersion and kind, as well as a boolean indicating whether the resources
// are namespaced.
GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error)
}
type clientProviderFromConfig struct {
config *rest.Config
}
func (c clientProviderFromConfig) GetClientFor(apiVersion, kind string) (dynamic.NamespaceableResourceInterface, bool, error) {
return getDynamicClientOnKind(apiVersion, kind, c.config)
}
func newLookupFunction(clientProvider ClientProvider) lookupFunc {
return func(apiversion string, kind string, namespace string, name string) (map[string]interface{}, error) {
var client dynamic.ResourceInterface
c, namespaced, err := getDynamicClientOnKind(apiversion, resource, config)
c, namespaced, err := clientProvider.GetClientFor(apiversion, kind)
if err != nil {
return map[string]interface{}{}, err
}

@ -17,6 +17,7 @@ package getter
import (
"bytes"
"fmt"
"os"
"os/exec"
"path/filepath"
@ -62,6 +63,13 @@ type pluginGetter struct {
opts options
}
func (p *pluginGetter) setupOptionsEnv(env []string) []string {
env = append(env, fmt.Sprintf("HELM_PLUGIN_USERNAME=%s", p.opts.username))
env = append(env, fmt.Sprintf("HELM_PLUGIN_PASSWORD=%s", p.opts.password))
env = append(env, fmt.Sprintf("HELM_PLUGIN_PASS_CREDENTIALS_ALL=%t", p.opts.passCredentialsAll))
return env
}
// Get runs downloader plugin command
func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error) {
for _, opt := range options {
@ -71,7 +79,7 @@ func (p *pluginGetter) Get(href string, options ...Option) (*bytes.Buffer, error
argv := append(commands[1:], p.opts.certFile, p.opts.keyFile, p.opts.caFile, href)
prog := exec.Command(filepath.Join(p.base, commands[0]), argv...)
plugin.SetupPluginEnv(p.settings, p.name, p.base)
prog.Env = os.Environ()
prog.Env = p.setupOptionsEnv(os.Environ())
buf := bytes.NewBuffer(nil)
prog.Stdout = buf
prog.Stderr = os.Stderr

@ -65,4 +65,4 @@ Notable differences from .gitignore:
- The evaluation of escape sequences has not been tested for compatibility
- There is no support for '\!' as a special leading sequence.
*/
package ignore // import "helm.sh/helm/v3/internal/ignore"
package ignore // import "helm.sh/helm/v3/pkg/ignore"

@ -467,7 +467,7 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) Delete(resources ResourceList) (*Result, []error) {
return delete(c, resources, metav1.DeletePropagationBackground)
return rdelete(c, resources, metav1.DeletePropagationBackground)
}
// Delete deletes Kubernetes resources specified in the resources list with
@ -475,10 +475,10 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) {
// if one or more fail and collect any errors. All successfully deleted items
// will be returned in the `Deleted` ResourceList that is part of the result.
func (c *Client) DeleteWithPropagationPolicy(resources ResourceList, policy metav1.DeletionPropagation) (*Result, []error) {
return delete(c, resources, policy)
return rdelete(c, resources, policy)
}
func delete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) {
func rdelete(c *Client, resources ResourceList, propagation metav1.DeletionPropagation) (*Result, []error) {
var errs []error
res := &Result{}
mtx := sync.Mutex{}
@ -772,7 +772,7 @@ func (c *Client) waitForJob(obj runtime.Object, name string) (bool, error) {
if c.Type == batch.JobComplete && c.Status == "True" {
return true, nil
} else if c.Type == batch.JobFailed && c.Status == "True" {
return true, errors.Errorf("job failed: %s", c.Reason)
return true, errors.Errorf("job %s failed: %s", name, c.Reason)
}
}

@ -49,7 +49,7 @@ func (p *PrintingKubeClient) Create(resources kube.ResourceList) (*kube.Result,
return &kube.Result{Created: resources}, nil
}
func (p *PrintingKubeClient) Get(resources kube.ResourceList, related bool) (map[string][]runtime.Object, error) {
func (p *PrintingKubeClient) Get(resources kube.ResourceList, _ bool) (map[string][]runtime.Object, error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, err
@ -119,7 +119,7 @@ func (p *PrintingKubeClient) WaitAndGetCompletedPodPhase(_ string, _ time.Durati
// DeleteWithPropagationPolicy implements KubeClient delete.
//
// It only prints out the content to be deleted.
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, policy metav1.DeletionPropagation) (*kube.Result, []error) {
func (p *PrintingKubeClient) DeleteWithPropagationPolicy(resources kube.ResourceList, _ metav1.DeletionPropagation) (*kube.Result, []error) {
_, err := io.Copy(p.Out, bufferize(resources))
if err != nil {
return nil, []error{err}

@ -90,13 +90,6 @@ type ReadyChecker struct {
// IsReady will fetch the latest state of the object from the server prior to
// performing readiness checks, and it will return any error encountered.
func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, error) {
var (
// This defaults to true, otherwise we get to a point where
// things will always return false unless one of the objects
// that manages pods has been hit
ok = true
err error
)
switch value := AsVersioned(v).(type) {
case *corev1.Pod:
pod, err := c.client.CoreV1().Pods(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
@ -183,11 +176,30 @@ func (c *ReadyChecker) IsReady(ctx context.Context, v *resource.Info) (bool, err
if !c.statefulSetReady(sts) {
return false, nil
}
case *corev1.ReplicationController, *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet:
ok, err = c.podsReadyForObject(ctx, v.Namespace, value)
}
if !ok || err != nil {
return false, err
case *corev1.ReplicationController:
rc, err := c.client.CoreV1().ReplicationControllers(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.replicationControllerReady(rc) {
return false, nil
}
ready, err := c.podsReadyForObject(ctx, v.Namespace, value)
if !ready || err != nil {
return false, err
}
case *extensionsv1beta1.ReplicaSet, *appsv1beta2.ReplicaSet, *appsv1.ReplicaSet:
rs, err := c.client.AppsV1().ReplicaSets(v.Namespace).Get(ctx, v.Name, metav1.GetOptions{})
if err != nil {
return false, err
}
if !c.replicaSetReady(rs) {
return false, nil
}
ready, err := c.podsReadyForObject(ctx, v.Namespace, value)
if !ready || err != nil {
return false, err
}
}
return true, nil
}
@ -276,6 +288,16 @@ func (c *ReadyChecker) volumeReady(v *corev1.PersistentVolumeClaim) bool {
}
func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deployment) bool {
// Verify the replicaset readiness
if !c.replicaSetReady(rs) {
return false
}
// Verify the generation observed by the deployment controller matches the spec generation
if dep.Status.ObservedGeneration != dep.ObjectMeta.Generation {
c.log("Deployment is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", dep.Namespace, dep.Name, dep.Status.ObservedGeneration, dep.ObjectMeta.Generation)
return false
}
expectedReady := *dep.Spec.Replicas - deploymentutil.MaxUnavailable(*dep)
if !(rs.Status.ReadyReplicas >= expectedReady) {
c.log("Deployment is not ready: %s/%s. %d out of %d expected pods are ready", dep.Namespace, dep.Name, rs.Status.ReadyReplicas, expectedReady)
@ -285,6 +307,12 @@ func (c *ReadyChecker) deploymentReady(rs *appsv1.ReplicaSet, dep *appsv1.Deploy
}
func (c *ReadyChecker) daemonSetReady(ds *appsv1.DaemonSet) bool {
// Verify the generation observed by the daemonSet controller matches the spec generation
if ds.Status.ObservedGeneration != ds.ObjectMeta.Generation {
c.log("DaemonSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", ds.Namespace, ds.Name, ds.Status.ObservedGeneration, ds.ObjectMeta.Generation)
return false
}
// If the update strategy is not a rolling update, there will be nothing to wait for
if ds.Spec.UpdateStrategy.Type != appsv1.RollingUpdateDaemonSetStrategyType {
return true
@ -355,22 +383,22 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
}
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// Verify the generation observed by the statefulSet controller matches the spec generation
if sts.Status.ObservedGeneration != sts.ObjectMeta.Generation {
c.log("StatefulSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", sts.Namespace, sts.Name, sts.Status.ObservedGeneration, sts.ObjectMeta.Generation)
return false
}
// If the update strategy is not a rolling update, there will be nothing to wait for
if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType {
c.log("StatefulSet skipped ready check: %s/%s. updateStrategy is %v", sts.Namespace, sts.Name, sts.Spec.UpdateStrategy.Type)
return true
}
// Make sure the status is up-to-date with the StatefulSet changes
if sts.Status.ObservedGeneration < sts.Generation {
c.log("StatefulSet is not ready: %s/%s. update has not yet been observed", sts.Namespace, sts.Name)
return false
}
// Dereference all the pointers because StatefulSets like them
var partition int
// 1 is the default for replicas if not set
var replicas = 1
replicas := 1
// For some reason, even if the update strategy is a rolling update, the
// actual rollingUpdate field can be nil. If it is, we can safely assume
// there is no partition value
@ -409,6 +437,24 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
return true
}
func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool {
// Verify the generation observed by the replicationController controller matches the spec generation
if rc.Status.ObservedGeneration != rc.ObjectMeta.Generation {
c.log("ReplicationController is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", rc.Namespace, rc.Name, rc.Status.ObservedGeneration, rc.ObjectMeta.Generation)
return false
}
return true
}
func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool {
// Verify the generation observed by the replicaSet controller matches the spec generation
if rs.Status.ObservedGeneration != rs.ObjectMeta.Generation {
c.log("ReplicaSet is not ready: %s/%s. observedGeneration (%d) does not match spec generation (%d).", rs.Namespace, rs.Name, rs.Status.ObservedGeneration, rs.ObjectMeta.Generation)
return false
}
return true
}
func getPods(ctx context.Context, client kubernetes.Interface, namespace, selector string) ([]corev1.Pod, error) {
list, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: selector,

@ -43,27 +43,51 @@ func Test_ReadyChecker_deploymentReady(t *testing.T) {
{
name: "deployment is ready",
args: args{
rs: newReplicaSet("foo", 1, 1),
dep: newDeployment("foo", 1, 1, 0),
rs: newReplicaSet("foo", 1, 1, true),
dep: newDeployment("foo", 1, 1, 0, true),
},
want: true,
},
{
name: "deployment is not ready",
args: args{
rs: newReplicaSet("foo", 0, 0),
dep: newDeployment("foo", 1, 1, 0),
rs: newReplicaSet("foo", 0, 0, true),
dep: newDeployment("foo", 1, 1, 0, true),
},
want: false,
},
{
name: "deployment is ready when maxUnavailable is set",
args: args{
rs: newReplicaSet("foo", 2, 1),
dep: newDeployment("foo", 2, 1, 1),
rs: newReplicaSet("foo", 2, 1, true),
dep: newDeployment("foo", 2, 1, 1, true),
},
want: true,
},
{
name: "deployment is not ready when replicaset generations are out of sync",
args: args{
rs: newReplicaSet("foo", 1, 1, false),
dep: newDeployment("foo", 1, 1, 0, true),
},
want: false,
},
{
name: "deployment is not ready when deployment generations are out of sync",
args: args{
rs: newReplicaSet("foo", 1, 1, true),
dep: newDeployment("foo", 1, 1, 0, false),
},
want: false,
},
{
name: "deployment is not ready when generations are out of sync",
args: args{
rs: newReplicaSet("foo", 1, 1, false),
dep: newDeployment("foo", 1, 1, 0, false),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -75,6 +99,74 @@ func Test_ReadyChecker_deploymentReady(t *testing.T) {
}
}
func Test_ReadyChecker_replicaSetReady(t *testing.T) {
type args struct {
rs *appsv1.ReplicaSet
}
tests := []struct {
name string
args args
want bool
}{
{
name: "replicaSet is ready",
args: args{
rs: newReplicaSet("foo", 1, 1, true),
},
want: true,
},
{
name: "replicaSet is not ready when generations are out of sync",
args: args{
rs: newReplicaSet("foo", 1, 1, false),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewReadyChecker(fake.NewSimpleClientset(), nil)
if got := c.replicaSetReady(tt.args.rs); got != tt.want {
t.Errorf("replicaSetReady() = %v, want %v", got, tt.want)
}
})
}
}
func Test_ReadyChecker_replicationControllerReady(t *testing.T) {
type args struct {
rc *corev1.ReplicationController
}
tests := []struct {
name string
args args
want bool
}{
{
name: "replicationController is ready",
args: args{
rc: newReplicationController("foo", true),
},
want: true,
},
{
name: "replicationController is not ready when generations are out of sync",
args: args{
rc: newReplicationController("foo", false),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewReadyChecker(fake.NewSimpleClientset(), nil)
if got := c.replicationControllerReady(tt.args.rc); got != tt.want {
t.Errorf("replicationControllerReady() = %v, want %v", got, tt.want)
}
})
}
}
func Test_ReadyChecker_daemonSetReady(t *testing.T) {
type args struct {
ds *appsv1.DaemonSet
@ -87,31 +179,38 @@ func Test_ReadyChecker_daemonSetReady(t *testing.T) {
{
name: "daemonset is ready",
args: args{
ds: newDaemonSet("foo", 0, 1, 1, 1),
ds: newDaemonSet("foo", 0, 1, 1, 1, true),
},
want: true,
},
{
name: "daemonset is not ready",
args: args{
ds: newDaemonSet("foo", 0, 0, 1, 1),
ds: newDaemonSet("foo", 0, 0, 1, 1, true),
},
want: false,
},
{
name: "daemonset pods have not been scheduled successfully",
args: args{
ds: newDaemonSet("foo", 0, 0, 1, 0),
ds: newDaemonSet("foo", 0, 0, 1, 0, true),
},
want: false,
},
{
name: "daemonset is ready when maxUnavailable is set",
args: args{
ds: newDaemonSet("foo", 1, 1, 2, 2),
ds: newDaemonSet("foo", 1, 1, 2, 2, true),
},
want: true,
},
{
name: "daemonset is not ready when generations are out of sync",
args: args{
ds: newDaemonSet("foo", 0, 1, 1, 1, false),
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@ -135,63 +234,56 @@ func Test_ReadyChecker_statefulSetReady(t *testing.T) {
{
name: "statefulset is ready",
args: args{
sts: newStatefulSet("foo", 1, 0, 1, 1),
sts: newStatefulSet("foo", 1, 0, 1, 1, true),
},
want: true,
},
{
name: "statefulset is not ready",
args: args{
sts: newStatefulSet("foo", 1, 0, 0, 1),
sts: newStatefulSet("foo", 1, 0, 0, 1, true),
},
want: false,
},
{
name: "statefulset is ready when partition is specified",
args: args{
sts: newStatefulSet("foo", 2, 1, 2, 1),
sts: newStatefulSet("foo", 2, 1, 2, 1, true),
},
want: true,
},
{
name: "statefulset is not ready when partition is set",
args: args{
sts: newStatefulSet("foo", 2, 1, 1, 0),
sts: newStatefulSet("foo", 2, 1, 1, 0, true),
},
want: false,
},
{
name: "statefulset is ready when partition is set and no change in template",
args: args{
sts: newStatefulSet("foo", 2, 1, 2, 2),
sts: newStatefulSet("foo", 2, 1, 2, 2, true),
},
want: true,
},
{
name: "statefulset is ready when partition is greater than replicas",
args: args{
sts: newStatefulSet("foo", 1, 2, 1, 1),
sts: newStatefulSet("foo", 1, 2, 1, 1, true),
},
want: true,
},
{
name: "statefulset is not ready when status of latest generation has not yet been observed",
name: "statefulset is not ready when generations are out of sync",
args: args{
sts: newStatefulSetWithNewGeneration("foo", 1, 0, 1, 1),
},
want: false,
},
{
name: "statefulset is not ready when current revision for current replicas does not match update revision for updated replicas",
args: args{
sts: newStatefulSetWithUpdateRevision("foo", 1, 0, 1, 1, "foo-bbbbbbb"),
sts: newStatefulSet("foo", 1, 0, 1, 1, false),
},
want: false,
},
{
name: "statefulset is ready when current revision for current replicas does not match update revision for updated replicas when using partition !=0",
args: args{
sts: newStatefulSetWithUpdateRevision("foo", 3, 2, 3, 3, "foo-bbbbbbb"),
sts: newStatefulSetWithUpdateRevision("foo", 3, 2, 3, 3, "foo-bbbbbbb", true),
},
want: true,
},
@ -222,7 +314,7 @@ func Test_ReadyChecker_podsReadyForObject(t *testing.T) {
name: "pods ready for a replicaset",
args: args{
namespace: defaultNamespace,
obj: newReplicaSet("foo", 1, 1),
obj: newReplicaSet("foo", 1, 1, true),
},
existPods: []corev1.Pod{
*newPodWithCondition("foo", corev1.ConditionTrue),
@ -234,7 +326,7 @@ func Test_ReadyChecker_podsReadyForObject(t *testing.T) {
name: "pods not ready for a replicaset",
args: args{
namespace: defaultNamespace,
obj: newReplicaSet("foo", 1, 1),
obj: newReplicaSet("foo", 1, 1, true),
},
existPods: []corev1.Pod{
*newPodWithCondition("foo", corev1.ConditionFalse),
@ -371,11 +463,22 @@ func Test_ReadyChecker_volumeReady(t *testing.T) {
}
}
func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberScheduled, updatedNumberScheduled int) *appsv1.DaemonSet {
func newStatefulSetWithUpdateRevision(name string, replicas, partition, readyReplicas, updatedReplicas int, updateRevision string, generationInSync bool) *appsv1.StatefulSet {
ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas, generationInSync)
ss.Status.UpdateRevision = updateRevision
return ss
}
func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberScheduled, updatedNumberScheduled int, generationInSync bool) *appsv1.DaemonSet {
var generation, observedGeneration int64 = 1, 1
if !generationInSync {
generation = 2
}
return &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: defaultNamespace,
Name: name,
Namespace: defaultNamespace,
Generation: generation,
},
Spec: appsv1.DaemonSetSpec{
UpdateStrategy: appsv1.DaemonSetUpdateStrategy{
@ -403,16 +506,21 @@ func newDaemonSet(name string, maxUnavailable, numberReady, desiredNumberSchedul
DesiredNumberScheduled: int32(desiredNumberScheduled),
NumberReady: int32(numberReady),
UpdatedNumberScheduled: int32(updatedNumberScheduled),
ObservedGeneration: observedGeneration,
},
}
}
func newStatefulSet(name string, replicas, partition, readyReplicas, updatedReplicas int) *appsv1.StatefulSet {
func newStatefulSet(name string, replicas, partition, readyReplicas, updatedReplicas int, generationInSync bool) *appsv1.StatefulSet {
var generation, observedGeneration int64 = 1, 1
if !generationInSync {
generation = 2
}
return &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: defaultNamespace,
Generation: int64(1),
Generation: generation,
},
Spec: appsv1.StatefulSetSpec{
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
@ -438,32 +546,23 @@ func newStatefulSet(name string, replicas, partition, readyReplicas, updatedRepl
},
},
Status: appsv1.StatefulSetStatus{
ObservedGeneration: int64(1),
CurrentRevision: name + "-aaaaaaa",
UpdateRevision: name + "-aaaaaaa",
UpdatedReplicas: int32(updatedReplicas),
ReadyReplicas: int32(readyReplicas),
ObservedGeneration: observedGeneration,
},
}
}
func newStatefulSetWithNewGeneration(name string, replicas, partition, readyReplicas, updatedReplicas int) *appsv1.StatefulSet {
ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas)
ss.Generation++
return ss
}
func newStatefulSetWithUpdateRevision(name string, replicas, partition, readyReplicas, updatedReplicas int, updateRevision string) *appsv1.StatefulSet {
ss := newStatefulSet(name, replicas, partition, readyReplicas, updatedReplicas)
ss.Status.UpdateRevision = updateRevision
return ss
}
func newDeployment(name string, replicas, maxSurge, maxUnavailable int) *appsv1.Deployment {
func newDeployment(name string, replicas, maxSurge, maxUnavailable int, generationInSync bool) *appsv1.Deployment {
var generation, observedGeneration int64 = 1, 1
if !generationInSync {
generation = 2
}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: defaultNamespace,
Name: name,
Namespace: defaultNamespace,
Generation: generation,
},
Spec: appsv1.DeploymentSpec{
Strategy: appsv1.DeploymentStrategy{
@ -489,17 +588,37 @@ func newDeployment(name string, replicas, maxSurge, maxUnavailable int) *appsv1.
},
},
},
Status: appsv1.DeploymentStatus{
ObservedGeneration: observedGeneration,
},
}
}
func newReplicaSet(name string, replicas int, readyReplicas int) *appsv1.ReplicaSet {
d := newDeployment(name, replicas, 0, 0)
func newReplicationController(name string, generationInSync bool) *corev1.ReplicationController {
var generation, observedGeneration int64 = 1, 1
if !generationInSync {
generation = 2
}
return &corev1.ReplicationController{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Generation: generation,
},
Status: corev1.ReplicationControllerStatus{
ObservedGeneration: observedGeneration,
},
}
}
func newReplicaSet(name string, replicas int, readyReplicas int, generationInSync bool) *appsv1.ReplicaSet {
d := newDeployment(name, replicas, 0, 0, generationInSync)
return &appsv1.ReplicaSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: defaultNamespace,
Labels: d.Spec.Selector.MatchLabels,
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(d, d.GroupVersionKind())},
Generation: d.Generation,
},
Spec: appsv1.ReplicaSetSpec{
Selector: d.Spec.Selector,
@ -507,7 +626,8 @@ func newReplicaSet(name string, replicas int, readyReplicas int) *appsv1.Replica
Template: d.Spec.Template,
},
Status: appsv1.ReplicaSetStatus{
ReadyReplicas: int32(readyReplicas),
ReadyReplicas: int32(readyReplicas),
ObservedGeneration: d.Status.ObservedGeneration,
},
}
}

@ -19,19 +19,25 @@ package lint // import "helm.sh/helm/v3/pkg/lint"
import (
"path/filepath"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/lint/rules"
"helm.sh/helm/v3/pkg/lint/support"
)
// All runs all of the available linters on the given base directory.
func All(basedir string, values map[string]interface{}, namespace string, strict bool) support.Linter {
func All(basedir string, values map[string]interface{}, namespace string, _ bool) support.Linter {
return AllWithKubeVersion(basedir, values, namespace, nil)
}
// AllWithKubeVersion runs all the available linters on the given base directory, allowing to specify the kubernetes version.
func AllWithKubeVersion(basedir string, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) support.Linter {
// Using abs path to get directory context
chartDir, _ := filepath.Abs(basedir)
linter := support.Linter{ChartDir: chartDir}
rules.Chartfile(&linter)
rules.ValuesWithOverrides(&linter, values)
rules.Templates(&linter, values, namespace, strict)
rules.TemplatesWithKubeVersion(&linter, values, namespace, kubeVersion)
rules.Dependencies(&linter)
return linter
}

@ -37,6 +37,7 @@ func Dependencies(linter *support.Linter) {
}
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependencyInMetadata(c))
linter.RunLinterRule(support.ErrorSev, linter.ChartDir, validateDependenciesUnique(c))
linter.RunLinterRule(support.WarningSev, linter.ChartDir, validateDependencyInChartsDir(c))
}
@ -80,3 +81,23 @@ func validateDependencyInMetadata(c *chart.Chart) (err error) {
}
return err
}
func validateDependenciesUnique(c *chart.Chart) (err error) {
dependencies := map[string]*chart.Dependency{}
shadowing := []string{}
for _, dep := range c.Metadata.Dependencies {
key := dep.Name
if dep.Alias != "" {
key = dep.Alias
}
if dependencies[key] != nil {
shadowing = append(shadowing, key)
}
dependencies[key] = dep
}
if len(shadowing) > 0 {
err = fmt.Errorf("multiple dependencies with name or alias: %s", strings.Join(shadowing, ","))
}
return err
}

@ -76,6 +76,67 @@ func TestValidateDependencyInMetadata(t *testing.T) {
}
}
func TestValidateDependenciesUnique(t *testing.T) {
tests := []struct {
chart chart.Chart
}{
{chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "foo",
},
{
Name: "foo",
},
},
},
}},
{chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "foo",
Alias: "bar",
},
{
Name: "bar",
},
},
},
}},
{chart.Chart{
Metadata: &chart.Metadata{
Name: "badchart",
Version: "0.1.0",
APIVersion: "v2",
Dependencies: []*chart.Dependency{
{
Name: "foo",
Alias: "baz",
},
{
Name: "bar",
Alias: "baz",
},
},
},
}},
}
for _, tt := range tests {
if err := validateDependenciesUnique(&tt.chart); err == nil {
t.Errorf("chart should have been flagged for dependency shadowing")
}
}
}
func TestDependencies(t *testing.T) {
tmp := t.TempDir()

@ -24,6 +24,8 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/endpoints/deprecation"
kscheme "k8s.io/client-go/kubernetes/scheme"
"helm.sh/helm/v3/pkg/chartutil"
)
var (
@ -45,7 +47,7 @@ func (e deprecatedAPIError) Error() string {
return msg
}
func validateNoDeprecations(resource *K8sYamlStruct) error {
func validateNoDeprecations(resource *K8sYamlStruct, kubeVersion *chartutil.KubeVersion) error {
// if `resource` does not have an APIVersion or Kind, we cannot test it for deprecation
if resource.APIVersion == "" {
return nil
@ -54,6 +56,14 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
return nil
}
majorVersion := k8sVersionMajor
minorVersion := k8sVersionMinor
if kubeVersion != nil {
majorVersion = kubeVersion.Major
minorVersion = kubeVersion.Minor
}
runtimeObject, err := resourceToRuntimeObject(resource)
if err != nil {
// do not error for non-kubernetes resources
@ -62,11 +72,12 @@ func validateNoDeprecations(resource *K8sYamlStruct) error {
}
return err
}
maj, err := strconv.Atoi(k8sVersionMajor)
maj, err := strconv.Atoi(majorVersion)
if err != nil {
return err
}
min, err := strconv.Atoi(k8sVersionMinor)
min, err := strconv.Atoi(minorVersion)
if err != nil {
return err
}

@ -23,7 +23,7 @@ func TestValidateNoDeprecations(t *testing.T) {
APIVersion: "extensions/v1beta1",
Kind: "Deployment",
}
err := validateNoDeprecations(deprecated)
err := validateNoDeprecations(deprecated, nil)
if err == nil {
t.Fatal("Expected deprecated extension to be flagged")
}
@ -35,7 +35,7 @@ func TestValidateNoDeprecations(t *testing.T) {
if err := validateNoDeprecations(&K8sYamlStruct{
APIVersion: "v1",
Kind: "Pod",
}); err != nil {
}, nil); err != nil {
t.Errorf("Expected a v1 Pod to not be deprecated")
}
}

@ -45,7 +45,12 @@ var (
)
// Templates lints the templates in the Linter.
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, strict bool) {
func Templates(linter *support.Linter, values map[string]interface{}, namespace string, _ bool) {
TemplatesWithKubeVersion(linter, values, namespace, nil)
}
// TemplatesWithKubeVersion lints the templates in the Linter, allowing to specify the kubernetes version.
func TemplatesWithKubeVersion(linter *support.Linter, values map[string]interface{}, namespace string, kubeVersion *chartutil.KubeVersion) {
fpath := "templates/"
templatesPath := filepath.Join(linter.ChartDir, fpath)
@ -70,6 +75,11 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
Namespace: namespace,
}
caps := chartutil.DefaultCapabilities.Copy()
if kubeVersion != nil {
caps.KubeVersion = *kubeVersion
}
// lint ignores import-values
// See https://github.com/helm/helm/issues/9658
if err := chartutil.ProcessDependenciesWithMerge(chart, values); err != nil {
@ -80,7 +90,8 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
if err != nil {
return
}
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, nil)
valuesToRender, err := chartutil.ToRenderValues(chart, cvals, options, caps)
if err != nil {
linter.RunLinterRule(support.ErrorSev, fpath, err)
return
@ -150,7 +161,7 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace
// NOTE: set to warnings to allow users to support out-of-date kubernetes
// Refs https://github.com/helm/helm/issues/8596
linter.RunLinterRule(support.WarningSev, fpath, validateMetadataName(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct))
linter.RunLinterRule(support.WarningSev, fpath, validateNoDeprecations(yamlStruct, kubeVersion))
linter.RunLinterRule(support.ErrorSev, fpath, validateMatchSelector(yamlStruct, renderedContent))
linter.RunLinterRule(support.ErrorSev, fpath, validateListAnnotations(yamlStruct, renderedContent))

@ -44,7 +44,7 @@ type TestHTTPGetter struct {
MockError error
}
func (t *TestHTTPGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) {
func (t *TestHTTPGetter) Get(_ string, _ ...getter.Option) (*bytes.Buffer, error) {
return t.MockResponse, t.MockError
}

@ -91,6 +91,7 @@ func TestPlatformPrepareCommand(t *testing.T) {
{OperatingSystem: "linux", Architecture: "arm64", Command: "echo -n linux-arm64"},
{OperatingSystem: "linux", Architecture: "ppc64le", Command: "echo -n linux-ppc64le"},
{OperatingSystem: "linux", Architecture: "s390x", Command: "echo -n linux-s390x"},
{OperatingSystem: "linux", Architecture: "riscv64", Command: "echo -n linux-riscv64"},
{OperatingSystem: "windows", Architecture: "amd64", Command: "echo -n win-64"},
},
},
@ -108,6 +109,8 @@ func TestPlatformPrepareCommand(t *testing.T) {
osStrCmp = "linux-ppc64le"
} else if os == "linux" && arch == "s390x" {
osStrCmp = "linux-s390x"
} else if os == "linux" && arch == "riscv64" {
osStrCmp = "linux-riscv64"
} else if os == "windows" && arch == "amd64" {
osStrCmp = "win-64"
} else {

@ -116,7 +116,7 @@ var ociProvider = Provider{
// All finds all of the registered pushers as a list of Provider instances.
// Currently, just the built-in pushers are collected.
func All(settings *cli.EnvSettings) Providers {
func All(_ *cli.EnvSettings) Providers {
result := Providers{ociProvider}
return result
}

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

@ -115,7 +115,7 @@ type CustomGetter struct {
repoUrls []string
}
func (g *CustomGetter) Get(href string, options ...getter.Option) (*bytes.Buffer, error) {
func (g *CustomGetter) Get(href string, _ ...getter.Option) (*bytes.Buffer, error) {
index := &IndexFile{
APIVersion: "v1",
Generated: time.Now(),

@ -445,7 +445,7 @@ func verifyLocalIndex(t *testing.T, i *IndexFile) {
}
func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *IndexFile) {
var expected, real []string
var expected, reald []string
for chart := range indexContent.Entries {
expected = append(expected, chart)
}
@ -453,12 +453,12 @@ func verifyLocalChartsFile(t *testing.T, chartsContent []byte, indexContent *Ind
scanner := bufio.NewScanner(bytes.NewReader(chartsContent))
for scanner.Scan() {
real = append(real, scanner.Text())
reald = append(reald, scanner.Text())
}
sort.Strings(real)
sort.Strings(reald)
if strings.Join(expected, " ") != strings.Join(real, " ") {
t.Errorf("Cached charts file content unexpected. Expected:\n%s\ngot:\n%s", expected, real)
if strings.Join(expected, " ") != strings.Join(reald, " ") {
t.Errorf("Cached charts file content unexpected. Expected:\n%s\ngot:\n%s", expected, reald)
}
}

@ -253,7 +253,7 @@ func (mock *MockSecretsInterface) Delete(_ context.Context, name string, _ metav
}
// newTestFixtureSQL mocks the SQL database (for testing purposes)
func newTestFixtureSQL(t *testing.T, releases ...*rspb.Release) (*SQL, sqlmock.Sqlmock) {
func newTestFixtureSQL(t *testing.T, _ ...*rspb.Release) (*SQL, sqlmock.Sqlmock) {
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("error when opening stub database connection: %v", err)

@ -662,7 +662,7 @@ func (s *SQL) Delete(key string) (*rspb.Release, error) {
}
// Get release custom labels from database
func (s *SQL) getReleaseCustomLabels(key string, namespace string) (map[string]string, error) {
func (s *SQL) getReleaseCustomLabels(key string, _ string) (map[string]string, error) {
query, args, err := s.statementBuilder.
Select(sqlCustomLabelsTableKeyColumn, sqlCustomLabelsTableValueColumn).
From(sqlCustomLabelsTableName).

@ -293,7 +293,7 @@ func (d *MaxHistoryMockDriver) Create(key string, rls *rspb.Release) error {
func (d *MaxHistoryMockDriver) Update(key string, rls *rspb.Release) error {
return d.Driver.Update(key, rls)
}
func (d *MaxHistoryMockDriver) Delete(key string) (*rspb.Release, error) {
func (d *MaxHistoryMockDriver) Delete(_ string) (*rspb.Release, error) {
return nil, errMaxHistoryMockDriverSomethingHappened
}
func (d *MaxHistoryMockDriver) Get(key string) (*rspb.Release, error) {

@ -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\nwindows-amd64"
local supported="darwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64"
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"

@ -68,7 +68,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\nwindows-amd64"
local supported="darwin-amd64\ndarwin-arm64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nlinux-s390x\nlinux-riscv64\nwindows-amd64"
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"

@ -89,6 +89,7 @@ Download Helm ${RELEASE}. The common platform binaries are here:
- [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 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))
- [Windows amd64](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip) ([checksum](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip.sha256sum) / $(cat _dist/helm-${RELEASE}-windows-amd64.zip.sha256))
The [Quickstart Guide](https://helm.sh/docs/intro/quickstart/) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://helm.sh/docs/intro/install/). You can also use a [script to install](https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3) on any system with \`bash\`.

Loading…
Cancel
Save