diff --git a/.circleci/bootstrap.sh b/.circleci/bootstrap.sh new file mode 100755 index 000000000..79d194077 --- /dev/null +++ b/.circleci/bootstrap.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# Copyright The Helm Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -euo pipefail + +curl -sSL https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | tar xz +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 diff --git a/.circleci/config.yml b/.circleci/config.yml index db7182adb..ef19b8ee7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,9 +9,13 @@ jobs: environment: GOCACHE: "/tmp/go/cache" + GOLANGCI_LINT_VERSION: "1.21.0" steps: - checkout + - run: + name: install test dependencies + command: .circleci/bootstrap.sh - run: name: test style command: make test-style diff --git a/.golangci.yml b/.golangci.yml index 601d98661..2c3b6234d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,5 @@ run: - deadline: 2m + timeout: 2m linters: disable-all: true diff --git a/Makefile b/Makefile index b52464ba9..611222e28 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ -BINDIR := $(CURDIR)/bin -DIST_DIRS := find * -type d -exec -TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64 -BINNAME ?= helm +BINDIR := $(CURDIR)/bin +DIST_DIRS := find * -type d -exec +TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64 +TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-386.tar.gz linux-386.tar.gz.sha256 linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256 +BINNAME ?= helm GOPATH = $(shell go env GOPATH) DEP = $(GOPATH)/bin/dep GOX = $(GOPATH)/bin/gox GOIMPORTS = $(GOPATH)/bin/goimports -GOLANGCI_LINT = $(GOPATH)/bin/golangci-lint ACCEPTANCE_DIR:=$(GOPATH)/src/helm.sh/acceptance-testing # To specify the subset of acceptance tests to run. '.' means all tests @@ -81,8 +81,8 @@ test-coverage: @ ./scripts/coverage.sh .PHONY: test-style -test-style: $(GOLANGCI_LINT) - GO111MODULE=on $(GOLANGCI_LINT) run +test-style: + GO111MODULE=on golangci-lint run @scripts/validate-license.sh .PHONY: test-acceptance @@ -118,9 +118,6 @@ format: $(GOIMPORTS) $(GOX): (cd /; GO111MODULE=on go get -u github.com/mitchellh/gox) -$(GOLANGCI_LINT): - (cd /; GO111MODULE=on go get -u github.com/golangci/golangci-lint/cmd/golangci-lint) - $(GOIMPORTS): (cd /; GO111MODULE=on go get -u golang.org/x/tools/cmd/goimports) @@ -142,6 +139,20 @@ dist: $(DIST_DIRS) zip -r helm-${VERSION}-{}.zip {} \; \ ) +.PHONY: fetch-dist +fetch-dist: + mkdir -p _dist + cd _dist && \ + for obj in ${TARGET_OBJS} ; do \ + curl -sSL -o helm-${VERSION}-$${obj} https://get.helm.sh/helm-${VERSION}-$${obj} ; \ + done + +.PHONY: sign +sign: + for f in _dist/*.{gz,zip,sha256} ; do \ + gpg --armor --detach-sign $${f} ; \ + done + .PHONY: checksum checksum: for f in _dist/*.{gz,zip} ; do \ diff --git a/README.md b/README.md index 9af358e10..73fd189d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CircleCI](https://circleci.com/gh/helm/helm.svg?style=shield)](https://circleci.com/gh/helm/helm) [![Go Report Card](https://goreportcard.com/badge/github.com/helm/helm)](https://goreportcard.com/report/github.com/helm/helm) -[![GoDoc](https://godoc.org/helm.sh/helm?status.svg)](https://godoc.org/helm.sh/helm) +[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/helm.sh/helm/v3) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/3131/badge)](https://bestpractices.coreinfrastructure.org/projects/3131) Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources. @@ -37,19 +37,19 @@ Unpack the `helm` binary and add it to your PATH and you are good to go! If you want to use a package manager: -- [Homebrew](https://brew.sh/) users can use `brew install kubernetes-helm`. +- [Homebrew](https://brew.sh/) users can use `brew install helm`. - [Chocolatey](https://chocolatey.org/) users can use `choco install kubernetes-helm`. - [Scoop](https://scoop.sh/) users can use `scoop install helm`. - [GoFish](https://gofi.sh/) users can use `gofish install helm`. To rapidly get Helm up and running, start with the [Quick Start Guide](https://docs.helm.sh/using_helm/#quickstart-guide). -See the [installation guide](https://docs.helm.sh/using_helm/#installing-helm) for more options, +See the [installation guide](https://helm.sh/docs/intro/install/) for more options, including installing pre-releases. ## Docs -Get started with the [Quick Start guide](https://docs.helm.sh/using_helm/#quickstart-guide) or plunge into the [complete documentation](https://docs.helm.sh) +Get started with the [Quick Start guide](https://helm.sh/docs/intro/quickstart/) or plunge into the [complete documentation](https://helm.sh/docs) ## Roadmap diff --git a/cmd/helm/install.go b/cmd/helm/install.go index 40935db17..0c14c013b 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -130,7 +130,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install") f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install") - f.BoolVar(&client.Replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") + f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&client.Wait, "wait", false, "if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout") f.BoolVarP(&client.GenerateName, "generate-name", "g", false, "generate the name (and omit the NAME parameter)") diff --git a/cmd/helm/list.go b/cmd/helm/list.go index 32501530d..0da0c21d3 100644 --- a/cmd/helm/list.go +++ b/cmd/helm/list.go @@ -77,12 +77,15 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.SetStateMask() results, err := client.Run() + if err != nil { + return err + } if client.Short { for _, res := range results { fmt.Fprintln(out, res.Name) } - return err + return nil } return outfmt.Write(out, newReleaseListWriter(results)) @@ -94,7 +97,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.BoolVarP(&client.ByDate, "date", "d", false, "sort by release date") f.BoolVarP(&client.SortReverse, "reverse", "r", false, "reverse the sort order") f.BoolVarP(&client.All, "all", "a", false, "show all releases, not just the ones marked deployed or failed") - f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases") + f.BoolVar(&client.Uninstalled, "uninstalled", false, "show uninstalled releases (if 'helm uninstall --keep-history' was used)") f.BoolVar(&client.Superseded, "superseded", false, "show superseded releases") f.BoolVar(&client.Uninstalling, "uninstalling", false, "show releases that are currently being uninstalled") f.BoolVar(&client.Deployed, "deployed", false, "show deployed releases. If no other is specified, this will be automatically enabled") diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index 53e082e96..f2fb5c01d 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -127,7 +127,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) { func manuallyProcessArgs(args []string) ([]string, []string) { known := []string{} unknown := []string{} - kvargs := []string{"--kube-context", "--namespace", "--kubeconfig", "--registry-config", "--repository-cache", "--repository-config"} + kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--registry-config", "--repository-cache", "--repository-config"} knownArg := func(a string) bool { for _, pre := range kvargs { if strings.HasPrefix(a, pre+"=") { @@ -136,13 +136,26 @@ func manuallyProcessArgs(args []string) ([]string, []string) { } return false } + + isKnown := func(v string) string { + for _, i := range kvargs { + if i == v { + return v + } + } + return "" + } + for i := 0; i < len(args); i++ { switch a := args[i]; a { case "--debug": known = append(known, a) - case "--kube-context", "--namespace", "-n", "--kubeconfig", "--registry-config", "--repository-cache", "--repository-config": - known = append(known, a, args[i+1]) + case isKnown(a): + known = append(known, a) i++ + if i < len(args) { + known = append(known, args[i]) + } default: if knownArg(a) { known = append(known, a) diff --git a/cmd/helm/package.go b/cmd/helm/package.go index dd4bb2d17..9f7961f95 100644 --- a/cmd/helm/package.go +++ b/cmd/helm/package.go @@ -36,10 +36,15 @@ This command packages a chart into a versioned chart archive file. If a path is given, this will look at that path for a chart (which must contain a Chart.yaml file) and then package that directory. -If no path is given, this will look in the present working directory for a -Chart.yaml file, and (if found) build the current directory into a chart. - Versioned chart archives are used by Helm package repositories. + +To sign a chart, use the '--sign' flag. In most cases, you should also +provide '--keyring path/to/secret/keys' and '--key keyname'. + + $ helm package --sign ./mychart --key mykey --keyring ~/.gnupg/secring.gpg + +If '--keyring' is not specified, Helm usually defaults to the public keyring +unless your environment is otherwise configured. ` func newPackageCmd(out io.Writer) *cobra.Command { diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 7bc8fe70c..3fd3a4197 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -30,14 +30,27 @@ func TestManuallyProcessArgs(t *testing.T) { "--debug", "--foo", "bar", "--kubeconfig=/home/foo", + "--kubeconfig", "/home/foo", + "--kube-context=test1", "--kube-context", "test1", + "-n=test2", "-n", "test2", + "--namespace=test2", + "--namespace", "test2", "--home=/tmp", "command", } expectKnown := []string{ - "--debug", "--kubeconfig=/home/foo", "--kube-context", "test1", "-n", "test2", + "--debug", + "--kubeconfig=/home/foo", + "--kubeconfig", "/home/foo", + "--kube-context=test1", + "--kube-context", "test1", + "-n=test2", + "-n", "test2", + "--namespace=test2", + "--namespace", "test2", } expectUnknown := []string{ diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index 1c84ad8d6..e6afce3d5 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -29,7 +29,7 @@ import ( "github.com/gofrs/flock" "github.com/pkg/errors" "github.com/spf13/cobra" - "gopkg.in/yaml.v2" + "sigs.k8s.io/yaml" "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/pkg/getter" diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 5312d798f..a47631c0d 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -44,6 +44,7 @@ faked locally. Additionally, none of the server-side testing of chart validity func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { var validate bool + var includeCrds bool client := action.NewInstall(cfg) valueOpts := &values.Options{} var extraAPIs []string @@ -67,7 +68,14 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { var manifests bytes.Buffer + if includeCrds { + for _, f := range rel.Chart.CRDs() { + fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", f.Name, f.Data) + } + } + fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest)) + if !client.DisableHooks { for _, m := range rel.Hooks { fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest) @@ -120,6 +128,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates") f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout") f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install") + f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output") + f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall") f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions") return cmd diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index 8aa2f6c06..735829432 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -79,6 +79,11 @@ func TestTemplateCmd(t *testing.T) { cmd: fmt.Sprintf("template --api-versions helm.k8s.io/test '%s'", chartPath), golden: "output/template-with-api-version.txt", }, + { + name: "template with CRDs", + cmd: fmt.Sprintf("template '%s' --include-crds", chartPath), + golden: "output/template-with-crds.txt", + }, } runTestCmd(t, tests) } diff --git a/cmd/helm/testdata/output/template-with-crds.txt b/cmd/helm/testdata/output/template-with-crds.txt new file mode 100644 index 000000000..9fa1c7e6d --- /dev/null +++ b/cmd/helm/testdata/output/template-with-crds.txt @@ -0,0 +1,72 @@ +--- +# Source: crds/crdA.yaml +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: testCRDs +spec: + group: testCRDGroups + names: + kind: TestCRD + listKind: TestCRDList + plural: TestCRDs + shortNames: + - tc + singular: authconfig + +--- +# Source: subchart1/charts/subcharta/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subcharta + labels: + helm.sh/chart: "subcharta-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: apache + selector: + app.kubernetes.io/name: subcharta +--- +# Source: subchart1/charts/subchartb/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchartb + labels: + helm.sh/chart: "subchartb-0.1.0" +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchartb +--- +# Source: subchart1/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: subchart1 + labels: + helm.sh/chart: "subchart1-0.1.0" + app.kubernetes.io/instance: "RELEASE-NAME" + kube-version/major: "1" + kube-version/minor: "16" + kube-version/version: "v1.16.0" + kube-api-version/test: v1 +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: 80 + protocol: TCP + name: nginx + selector: + app.kubernetes.io/name: subchart1 diff --git a/code-of-conduct.md b/code-of-conduct.md index 0d15c00cf..91ccaf035 100644 --- a/code-of-conduct.md +++ b/code-of-conduct.md @@ -1,3 +1,3 @@ -# Kubernetes Community Code of Conduct +# Community Code of Conduct -Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) +Helm follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). diff --git a/internal/tlsutil/cfg.go b/internal/tlsutil/cfg.go index f40258739..8b9d4329f 100644 --- a/internal/tlsutil/cfg.go +++ b/internal/tlsutil/cfg.go @@ -27,18 +27,14 @@ import ( // Options represents configurable options used to create client and server TLS configurations. type Options struct { CaCertFile string - // If either the KeyFile or CertFile is empty, ClientConfig() will not load them, - // preventing Helm from authenticating to Tiller. They are required to be non-empty - // when calling ServerConfig, otherwise an error is returned. + // If either the KeyFile or CertFile is empty, ClientConfig() will not load them. KeyFile string CertFile string // Client-only options InsecureSkipVerify bool - // Server-only options - ClientAuth tls.ClientAuthType } -// ClientConfig retusn a TLS configuration for use by a Helm client. +// ClientConfig returns a TLS configuration for use by a Helm client. func ClientConfig(opts Options) (cfg *tls.Config, err error) { var cert *tls.Certificate var pool *x509.CertPool @@ -60,24 +56,3 @@ func ClientConfig(opts Options) (cfg *tls.Config, err error) { cfg = &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify, Certificates: []tls.Certificate{*cert}, RootCAs: pool} return cfg, nil } - -// ServerConfig returns a TLS configuration for use by the Tiller server. -func ServerConfig(opts Options) (cfg *tls.Config, err error) { - var cert *tls.Certificate - var pool *x509.CertPool - - if cert, err = CertFromFilePair(opts.CertFile, opts.KeyFile); err != nil { - if os.IsNotExist(err) { - return nil, errors.Wrapf(err, "could not load x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile) - } - return nil, errors.Wrapf(err, "could not read x509 key pair (cert: %q, key: %q)", opts.CertFile, opts.KeyFile) - } - if opts.ClientAuth >= tls.VerifyClientCertIfGiven && opts.CaCertFile != "" { - if pool, err = CertPoolFromFile(opts.CaCertFile); err != nil { - return nil, err - } - } - - cfg = &tls.Config{MinVersion: tls.VersionTLS12, ClientAuth: opts.ClientAuth, Certificates: []tls.Certificate{*cert}, ClientCAs: pool} - return cfg, nil -} diff --git a/internal/tlsutil/tls.go b/internal/tlsutil/tls.go index dc123e1e5..ed7795dbe 100644 --- a/internal/tlsutil/tls.go +++ b/internal/tlsutil/tls.go @@ -26,13 +26,16 @@ import ( // NewClientTLS returns tls.Config appropriate for client auth. func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { - cert, err := CertFromFilePair(certFile, keyFile) - if err != nil { - return nil, err - } - config := tls.Config{ - Certificates: []tls.Certificate{*cert}, + config := tls.Config{} + + if certFile != "" && keyFile != "" { + cert, err := CertFromFilePair(certFile, keyFile) + if err != nil { + return nil, err + } + config.Certificates = []tls.Certificate{*cert} } + if caFile != "" { cp, err := CertPoolFromFile(caFile) if err != nil { @@ -40,6 +43,7 @@ func NewClientTLS(certFile, keyFile, caFile string) (*tls.Config, error) { } config.RootCAs = cp } + return &config, nil } diff --git a/internal/tlsutil/tlsutil_test.go b/internal/tlsutil/tlsutil_test.go index c2a477272..24551fdec 100644 --- a/internal/tlsutil/tlsutil_test.go +++ b/internal/tlsutil/tlsutil_test.go @@ -17,7 +17,6 @@ limitations under the License. package tlsutil import ( - "crypto/tls" "path/filepath" "testing" ) @@ -54,30 +53,61 @@ func TestClientConfig(t *testing.T) { } } -func TestServerConfig(t *testing.T) { - opts := Options{ - CaCertFile: testfile(t, testCaCertFile), - CertFile: testfile(t, testCertFile), - KeyFile: testfile(t, testKeyFile), - ClientAuth: tls.RequireAndVerifyClientCert, +func testfile(t *testing.T, file string) (path string) { + var err error + if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil { + t.Fatalf("error getting absolute path to test file %q: %v", file, err) } + return path +} + +func TestNewClientTLS(t *testing.T) { + certFile := testfile(t, testCertFile) + keyFile := testfile(t, testKeyFile) + caCertFile := testfile(t, testCaCertFile) - cfg, err := ServerConfig(opts) + cfg, err := NewClientTLS(certFile, keyFile, caCertFile) if err != nil { - t.Fatalf("error building tls server config: %v", err) + t.Error(err) } - if got := cfg.MinVersion; got != tls.VersionTLS12 { - t.Errorf("expecting TLS version 1.2, got %d", got) + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) } - if got := cfg.ClientCAs; got == nil { - t.Errorf("expecting non-nil CA pool") + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mistmatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") } -} -func testfile(t *testing.T, file string) (path string) { - var err error - if path, err = filepath.Abs(filepath.Join(tlsTestDir, file)); err != nil { - t.Fatalf("error getting absolute path to test file %q: %v", file, err) + cfg, err = NewClientTLS("", "", caCertFile) + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 0 { + t.Fatalf("expecting 0 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mistmatch, expecting false") + } + if cfg.RootCAs == nil { + t.Fatalf("mismatch tls RootCAs, expecting non-nil") + } + + cfg, err = NewClientTLS(certFile, keyFile, "") + if err != nil { + t.Error(err) + } + + if got := len(cfg.Certificates); got != 1 { + t.Fatalf("expecting 1 client certificates, got %d", got) + } + if cfg.InsecureSkipVerify { + t.Fatalf("insecure skip verify mistmatch, expecting false") + } + if cfg.RootCAs != nil { + t.Fatalf("mismatch tls RootCAs, expecting nil") } - return path } diff --git a/pkg/action/action.go b/pkg/action/action.go index 48d6bf742..f74a25e41 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -109,9 +109,19 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { if err != nil { return nil, errors.Wrap(err, "could not get server version from Kubernetes") } + // Issue #6361: + // Client-Go emits an error when an API service is registered but unimplemented. + // We trap that error here and print a warning. But since the discovery client continues + // building the API object, it is correctly populated with all valid APIs. + // See https://github.com/kubernetes/kubernetes/issues/72051#issuecomment-521157642 apiVersions, err := GetVersionSet(dc) if err != nil { - return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") + if discovery.IsGroupDiscoveryFailedError(err) { + c.Log("WARNING: The Kubernetes server has an orphaned API service. Server reports: %s", err) + c.Log("WARNING: To fix this, kubectl delete apiservice ") + } else { + return nil, errors.Wrap(err, "could not get apiVersions from Kubernetes") + } } c.Capabilities = &chartutil.Capabilities{ diff --git a/pkg/action/get_values.go b/pkg/action/get_values.go index 5bc3a7005..9c32db213 100644 --- a/pkg/action/get_values.go +++ b/pkg/action/get_values.go @@ -39,6 +39,10 @@ func NewGetValues(cfg *Configuration) *GetValues { // Run executes 'helm get values' against the given release. func (g *GetValues) Run(name string) (map[string]interface{}, error) { + if err := g.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + rel, err := g.cfg.releaseContent(name, g.Version) if err != nil { return nil, err diff --git a/pkg/action/install.go b/pkg/action/install.go index 7cb73a6d3..2fdc1ddf4 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -86,6 +86,8 @@ type Install struct { // APIVersions allows a manual set of supported API Versions to be passed // (for things like templating). These are ignored if ClientOnly is false APIVersions chartutil.VersionSet + // Used by helm template to render charts with .Relase.IsUpgrade. Ignored if Dry-Run is false + IsUpgrade bool } // ChartPathOptions captures common options used for controlling chart paths @@ -126,7 +128,7 @@ func (i *Install) installCRDs(crds []*chart.File) error { i.cfg.Log("CRD %s is already present. Skipping.", crdName) continue } - return errors.Wrapf(err, "failed to instal CRD %s", obj.Name) + return errors.Wrapf(err, "failed to install CRD %s", obj.Name) } totalItems = append(totalItems, res...) } @@ -193,11 +195,14 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return nil, err } + //special case for helm template --is-upgrade + isUpgrade := i.IsUpgrade && i.DryRun options := chartutil.ReleaseOptions{ Name: i.ReleaseName, Namespace: i.Namespace, Revision: 1, - IsInstall: true, + IsInstall: !isUpgrade, + IsUpgrade: isUpgrade, } valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps) if err != nil { @@ -233,7 +238,7 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // we'll end up in a state where we will delete those resources upon // deleting the release because the manifest will be pointing at that // resource - if !i.ClientOnly { + if !i.ClientOnly && !isUpgrade { if err := existingResourceConflict(resources); err != nil { return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install") } @@ -297,7 +302,9 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // // One possible strategy would be to do a timed retry to see if we can get // this stored in the future. - i.recordRelease(rel) + if err := i.recordRelease(rel); err != nil { + i.cfg.Log("failed to record the release: %s", err) + } return rel, nil } @@ -416,7 +423,7 @@ func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values if ch.Metadata.KubeVersion != "" { if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { - return hs, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) + return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) } } diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index bb36b843d..4637a4b10 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -306,7 +306,7 @@ func TestInstallRelease_KubeVersion(t *testing.T) { vals = map[string]interface{}{} _, err = instAction.Run(buildChart(withKube(">=99.0.0")), vals) is.Error(err) - is.Contains(err.Error(), "chart requires kubernetesVersion") + is.Contains(err.Error(), "chart requires kubeVersion") } func TestInstallRelease_Wait(t *testing.T) { diff --git a/pkg/action/pull.go b/pkg/action/pull.go index aaf63861e..b0a3d2598 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -63,6 +63,7 @@ func (p *Pull) Run(chartRef string) (string, error) { Getters: getter.All(p.Settings), Options: []getter.Option{ getter.WithBasicAuth(p.Username, p.Password), + getter.WithTLSClientConfig(p.CertFile, p.KeyFile, p.CaFile), }, RepositoryConfig: p.Settings.RepositoryConfig, RepositoryCache: p.Settings.RepositoryCache, diff --git a/pkg/chart/loader/archive.go b/pkg/chart/loader/archive.go index 3c50fe379..7e187a170 100644 --- a/pkg/chart/loader/archive.go +++ b/pkg/chart/loader/archive.go @@ -126,6 +126,12 @@ func LoadArchiveFiles(in io.Reader) ([]*BufferedFile, error) { continue } + switch hd.Typeflag { + // We don't want to process these extension header files. + case tar.TypeXGlobalHeader, tar.TypeXHeader: + continue + } + // Archive could contain \ if generated on Windows delimiter := "/" if strings.ContainsRune(hd.Name, '\\') { diff --git a/pkg/chart/loader/archive_test.go b/pkg/chart/loader/archive_test.go new file mode 100644 index 000000000..7d8c8b51e --- /dev/null +++ b/pkg/chart/loader/archive_test.go @@ -0,0 +1,110 @@ +/* +Copyright The Helm Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package loader + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "testing" +) + +func TestLoadArchiveFiles(t *testing.T) { + tcs := []struct { + name string + generate func(w *tar.Writer) + check func(t *testing.T, files []*BufferedFile, err error) + }{ + { + name: "empty input should return no files", + generate: func(w *tar.Writer) {}, + check: func(t *testing.T, files []*BufferedFile, err error) { + if err.Error() != "no files in chart archive" { + t.Fatalf(`expected "no files in chart archive", got [%#v]`, err) + } + }, + }, + { + name: "should ignore files with XGlobalHeader type", + generate: func(w *tar.Writer) { + // simulate the presence of a `pax_global_header` file like you would get when + // processing a GitHub release archive. + _ = w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeXGlobalHeader, + Name: "pax_global_header", + }) + + // we need to have at least one file, otherwise we'll get the "no files in chart archive" error + _ = w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: "dir/empty", + }) + }, + check: func(t *testing.T, files []*BufferedFile, err error) { + if err != nil { + t.Fatalf(`got unwanted error [%#v] for tar file with pax_global_header content`, err) + } + + if len(files) != 1 { + t.Fatalf(`expected to get one file but got [%v]`, files) + } + }, + }, + { + name: "should ignore files with TypeXHeader type", + generate: func(w *tar.Writer) { + // simulate the presence of a `pax_header` file like you might get when + // processing a GitHub release archive. + _ = w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeXHeader, + Name: "pax_header", + }) + + // we need to have at least one file, otherwise we'll get the "no files in chart archive" error + _ = w.WriteHeader(&tar.Header{ + Typeflag: tar.TypeReg, + Name: "dir/empty", + }) + }, + check: func(t *testing.T, files []*BufferedFile, err error) { + if err != nil { + t.Fatalf(`got unwanted error [%#v] for tar file with pax_header content`, err) + } + + if len(files) != 1 { + t.Fatalf(`expected to get one file but got [%v]`, files) + } + }, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + buf := &bytes.Buffer{} + gzw := gzip.NewWriter(buf) + tw := tar.NewWriter(gzw) + + tc.generate(tw) + + _ = tw.Close() + _ = gzw.Close() + + files, err := LoadArchiveFiles(buf) + tc.check(t, files, err) + }) + } +} diff --git a/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml b/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml new file mode 100644 index 000000000..fca77fd4b --- /dev/null +++ b/pkg/chartutil/testdata/subpop/charts/subchart1/crds/crdA.yaml @@ -0,0 +1,13 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: testCRDs +spec: + group: testCRDGroups + names: + kind: TestCRD + listKind: TestCRDList + plural: TestCRDs + shortNames: + - tc + singular: authconfig diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index 8e251bc89..f3d4321c5 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -214,6 +214,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password)) } + if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" { + c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) + } + // Next, we need to load the index, and actually look up the chart. idxFile := filepath.Join(c.RepositoryCache, helmpath.CacheIndexFile(r.Config.Name)) i, err := repo.LoadIndexFile(idxFile) diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index 80249e240..e0692c8c8 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -80,6 +80,67 @@ func TestResolveChartRef(t *testing.T) { } } +func TestResolveChartOpts(t *testing.T) { + tests := []struct { + name, ref, version string + expect []getter.Option + }{ + { + name: "repo with CA-file", + ref: "testing-ca-file/foo", + expect: []getter.Option{ + getter.WithURL("https://example.com/foo-1.2.3.tgz"), + getter.WithTLSClientConfig("cert", "key", "ca"), + }, + }, + } + + c := ChartDownloader{ + Out: os.Stderr, + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), + } + + // snapshot options + snapshotOpts := c.Options + + for _, tt := range tests { + // reset chart downloader options for each test case + c.Options = snapshotOpts + + expect, err := getter.NewHTTPGetter(tt.expect...) + if err != nil { + t.Errorf("%s: failed to setup http client: %s", tt.name, err) + continue + } + + u, err := c.ResolveChartVersion(tt.ref, tt.version) + if err != nil { + t.Errorf("%s: failed with error %s", tt.name, err) + continue + } + + got, err := getter.NewHTTPGetter( + append( + c.Options, + getter.WithURL(u.String()), + )..., + ) + if err != nil { + t.Errorf("%s: failed to create http client: %s", tt.name, err) + continue + } + + if *(got.(*getter.HTTPGetter)) != *(expect.(*getter.HTTPGetter)) { + t.Errorf("%s: expected %s, got %s", tt.name, expect, got) + } + } +} + func TestVerifyChart(t *testing.T) { v, err := VerifyChart("testdata/signtest-0.1.0.tgz", "testdata/helm-test-key.pub") if err != nil { diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index 407e7ffe4..81dd53614 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -79,7 +79,12 @@ func (m *Manager) Build() error { return m.Update() } + // Check that all of the repos we're dependent on actually exist. req := c.Metadata.Dependencies + if _, err := m.resolveRepoNames(req); err != nil { + return err + } + if sum, err := resolver.HashReq(req, lock.Dependencies); err != nil || sum != lock.Digest { return errors.New("Chart.lock is out of sync with Chart.yaml") } @@ -120,7 +125,7 @@ func (m *Manager) Update() error { // Check that all of the repos we're dependent on actually exist and // the repo index names. - repoNames, err := m.getRepoNames(req) + repoNames, err := m.resolveRepoNames(req) if err != nil { return err } @@ -144,6 +149,13 @@ func (m *Manager) Update() error { return err } + // downloadAll might overwrite dependency version, recalculate lock digest + newDigest, err := resolver.HashReq(req, lock.Dependencies) + if err != nil { + return err + } + lock.Digest = newDigest + // If the lock file hasn't changed, don't write a new one. oldLock := c.Lock if oldLock != nil && oldLock.Digest == lock.Digest { @@ -372,8 +384,9 @@ Loop: return nil } -// getRepoNames returns the repo names of the referenced deps which can be used to fetch the cahced index file. -func (m *Manager) getRepoNames(deps []*chart.Dependency) (map[string]string, error) { +// resolveRepoNames returns the repo names of the referenced deps which can be used to fetch the cached index file +// and replaces aliased repository URLs into resolved URLs in dependencies. +func (m *Manager) resolveRepoNames(deps []*chart.Dependency) (map[string]string, error) { rf, err := loadRepoConfig(m.RepositoryConfig) if err != nil { if os.IsNotExist(err) { diff --git a/pkg/downloader/manager_test.go b/pkg/downloader/manager_test.go index 0c5c08615..dd83c3dc2 100644 --- a/pkg/downloader/manager_test.go +++ b/pkg/downloader/manager_test.go @@ -161,7 +161,7 @@ func TestGetRepoNames(t *testing.T) { } for _, tt := range tests { - l, err := m.getRepoNames(tt.req) + l, err := m.resolveRepoNames(tt.req) if err != nil { if tt.err { continue @@ -181,7 +181,76 @@ func TestGetRepoNames(t *testing.T) { } } -// This function is the skeleton test code of failing tests for #6416 and bugs due to #5874. +func TestUpdateBeforeBuild(t *testing.T) { + // Set up a fake repo + srv, err := repotest.NewTempServer("testdata/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + dir := func(p ...string) string { + return filepath.Join(append([]string{srv.Root()}, p...)...) + } + + // Save dep + d := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "dep-chart", + Version: "0.1.0", + APIVersion: "v1", + }, + } + if err := chartutil.SaveDir(d, dir()); err != nil { + t.Fatal(err) + } + // Save a chart + c := &chart.Chart{ + Metadata: &chart.Metadata{ + Name: "with-dependency", + Version: "0.1.0", + APIVersion: "v1", + Dependencies: []*chart.Dependency{{ + Name: d.Metadata.Name, + Version: ">=0.1.0", + Repository: "file://../dep-chart", + }}, + }, + } + if err := chartutil.SaveDir(c, dir()); err != nil { + t.Fatal(err) + } + + // Set-up a manager + b := bytes.NewBuffer(nil) + g := getter.Providers{getter.Provider{ + Schemes: []string{"http", "https"}, + New: getter.NewHTTPGetter, + }} + m := &Manager{ + ChartPath: dir(c.Metadata.Name), + Out: b, + Getters: g, + RepositoryConfig: dir("repositories.yaml"), + RepositoryCache: dir(), + } + + // Update before Build. see issue: https://github.com/helm/helm/issues/7101 + err = m.Update() + if err != nil { + t.Fatal(err) + } + + err = m.Build() + if err != nil { + t.Fatal(err) + } +} + +// This function is the skeleton test code of failing tests for #6416 and #6871 and bugs due to #5874. +// // This function is used by below tests that ensures success of build operation // with optional fields, alias, condition, tags, and even with ranged version. // Parent chart includes local-subchart 0.1.0 subchart from a fake repository, by default. @@ -283,3 +352,11 @@ func TestBuild_WithTags(t *testing.T) { Tags: []string{"tag1", "tag2"}, }) } + +// Failing test for #6871 +func TestBuild_WithRepositoryAlias(t *testing.T) { + // Dependency repository is aliased in Chart.yaml + checkBuildWithOptionalFields(t, "with-repository-alias", chart.Dependency{ + Repository: "@test", + }) +} diff --git a/pkg/downloader/testdata/repositories.yaml b/pkg/downloader/testdata/repositories.yaml index 374d95c8a..430865269 100644 --- a/pkg/downloader/testdata/repositories.yaml +++ b/pkg/downloader/testdata/repositories.yaml @@ -15,4 +15,9 @@ repositories: - name: testing-relative url: "http://example.com/helm" - name: testing-relative-trailing-slash - url: "http://example.com/helm/" \ No newline at end of file + url: "http://example.com/helm/" + - name: testing-ca-file + url: "https://example.com" + certFile: "cert" + keyFile: "key" + caFile: "ca" diff --git a/pkg/downloader/testdata/repository/testing-ca-file-index.yaml b/pkg/downloader/testdata/repository/testing-ca-file-index.yaml new file mode 100644 index 000000000..17cdde1c6 --- /dev/null +++ b/pkg/downloader/testdata/repository/testing-ca-file-index.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +entries: + foo: + - name: foo + description: Foo Chart + home: https://helm.sh/helm + keywords: [] + maintainers: [] + sources: + - https://github.com/helm/charts + urls: + - https://example.com/foo-1.2.3.tgz + version: 1.2.3 + checksum: 0e6661f193211d7a5206918d42f5c2a9470b737d diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go index 559f54bed..54c866d16 100644 --- a/pkg/engine/engine.go +++ b/pkg/engine/engine.go @@ -99,11 +99,19 @@ func warnWrap(warn string) string { // 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([]string, 0) // Add the 'include' function here so we can close over t. funcMap["include"] = func(name string, data interface{}) (string, error) { var buf strings.Builder + for _, n := range includedNames { + if n == name { + return "", errors.Wrapf(fmt.Errorf("unable to excute template"), "rendering template has a nested reference name: %s", name) + } + } + includedNames = append(includedNames, name) err := t.ExecuteTemplate(&buf, name, data) + includedNames = includedNames[:len(includedNames)-1] return buf.String(), err } diff --git a/pkg/engine/engine_test.go b/pkg/engine/engine_test.go index ef2165a62..b14d42315 100644 --- a/pkg/engine/engine_test.go +++ b/pkg/engine/engine_test.go @@ -464,6 +464,15 @@ func TestAlterFuncMap_include(t *testing.T) { }, } + // Check nested reference in include FuncMap + d := &chart.Chart{ + Metadata: &chart.Metadata{Name: "nested"}, + Templates: []*chart.File{ + {Name: "templates/quote", Data: []byte(`{{include "nested/templates/quote" . | indent 2}} dead.`)}, + {Name: "templates/_partial", Data: []byte(`{{.Release.Name}} - he`)}, + }, + } + v := chartutil.Values{ "Values": map[string]interface{}{}, "Chart": c.Metadata, @@ -481,6 +490,12 @@ func TestAlterFuncMap_include(t *testing.T) { if got := out["conrad/templates/quote"]; got != expect { t.Errorf("Expected %q, got %q (%v)", expect, got, out) } + + _, err = Render(d, v) + expectErrName := "nested/templates/quote" + if err == nil { + t.Errorf("Expected err of nested reference name: %v", expectErrName) + } } func TestAlterFuncMap_require(t *testing.T) { diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index a36ab0321..89abfb1cf 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -89,7 +89,7 @@ func NewHTTPGetter(options ...Option) (Getter, error) { } func (g *HTTPGetter) httpClient() (*http.Client, error) { - if g.opts.certFile != "" && g.opts.keyFile != "" { + if (g.opts.certFile != "" && g.opts.keyFile != "") || g.opts.caFile != "" { tlsConf, err := tlsutil.NewClientTLS(g.opts.certFile, g.opts.keyFile, g.opts.caFile) if err != nil { return nil, errors.Wrap(err, "can't create TLS config for client") diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index 0c2c55ea3..b20085574 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -180,4 +180,14 @@ func TestDownloadTLS(t *testing.T) { if _, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig(pub, priv, ca)); err != nil { t.Error(err) } + + // test with only the CA file (see also #6635) + g, err = NewHTTPGetter() + if err != nil { + t.Fatal(err) + } + + if _, err := g.Get(u.String(), WithURL(u.String()), WithTLSClientConfig("", "", ca)); err != nil { + t.Error(err) + } } diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index 7198917c4..f0005a61e 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -27,6 +27,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" extensionsv1beta1 "k8s.io/api/extensions/v1beta1" + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -114,6 +115,17 @@ func (w *waiter) waitForResources(created ResourceList) error { if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil { return false, err } + if !w.crdBetaReady(*crd) { + return false, nil + } + case *apiextv1.CustomResourceDefinition: + if err := v.Get(); err != nil { + return false, err + } + crd := &apiextv1.CustomResourceDefinition{} + if err := scheme.Scheme.Convert(v.Object, crd, nil); err != nil { + return false, err + } if !w.crdReady(*crd) { return false, nil } @@ -229,7 +241,10 @@ func (w *waiter) daemonSetReady(ds *appsv1.DaemonSet) bool { return true } -func (w *waiter) crdReady(crd apiextv1beta1.CustomResourceDefinition) bool { +// Because the v1 extensions API is not available on all supported k8s versions +// yet and because Go doesn't support generics, we need to have a duplicate +// function to support the v1beta1 types +func (w *waiter) crdBetaReady(crd apiextv1beta1.CustomResourceDefinition) bool { for _, cond := range crd.Status.Conditions { switch cond.Type { case apiextv1beta1.Established: @@ -249,6 +264,26 @@ func (w *waiter) crdReady(crd apiextv1beta1.CustomResourceDefinition) bool { return false } +func (w *waiter) crdReady(crd apiextv1.CustomResourceDefinition) bool { + for _, cond := range crd.Status.Conditions { + switch cond.Type { + case apiextv1.Established: + if cond.Status == apiextv1.ConditionTrue { + return true + } + case apiextv1.NamesAccepted: + if cond.Status == apiextv1.ConditionFalse { + // This indicates a naming conflict, but it's probably not the + // job of this function to fail because of that. Instead, + // we treat it as a success, since the process should be able to + // continue. + return true + } + } + } + return false +} + func (w *waiter) statefulSetReady(sts *appsv1.StatefulSet) bool { // If the update strategy is not a rolling update, there will be nothing to wait for if sts.Spec.UpdateStrategy.Type != appsv1.RollingUpdateStatefulSetStrategyType { diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index 04e63a0c5..ac650ba57 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -35,12 +35,12 @@ const goodChartDir = "rules/testdata/goodone" func TestBadChart(t *testing.T) { m := All(badChartDir, values, namespace, strict).Messages - if len(m) != 9 { + if len(m) != 8 { t.Errorf("Number of errors %v", len(m)) t.Errorf("All didn't fail with expected errors, got %#v", m) } // There should be one INFO, 2 WARNINGs and one ERROR messages, check for them - var i, w, e, e2, e3, e4, e5, e6, e7 bool + var i, w, e, e2, e3, e4, e5, e6 bool for _, msg := range m { if msg.Severity == support.InfoSev { if strings.Contains(msg.Err.Error(), "icon is recommended") { @@ -59,28 +59,25 @@ func TestBadChart(t *testing.T) { if strings.Contains(msg.Err.Error(), "name is required") { e2 = true } - if strings.Contains(msg.Err.Error(), "directory name (badchartfile) and chart name () must be the same") { - e3 = true - } if strings.Contains(msg.Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { - e4 = true + e3 = true } if strings.Contains(msg.Err.Error(), "chart type is not valid in apiVersion") { - e5 = true + e4 = true } if strings.Contains(msg.Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { - e6 = true + e5 = true } if strings.Contains(msg.Err.Error(), "chart.metadata.name is required") { - e7 = true + e6 = true } } } - if !e || !e2 || !e3 || !e4 || !e5 || !e6 || !e7 || !w || !i { + if !e || !e2 || !e3 || !e4 || !e5 || !e6 || !w || !i { t.Errorf("Didn't find all the expected errors, got %#v", m) } } diff --git a/pkg/lint/rules/chartfile.go b/pkg/lint/rules/chartfile.go index 35bb81571..70cb83bc5 100644 --- a/pkg/lint/rules/chartfile.go +++ b/pkg/lint/rules/chartfile.go @@ -46,7 +46,6 @@ func Chartfile(linter *support.Linter) { } linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartName(chartFile)) - linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartNameDirMatch(linter.ChartDir, chartFile)) // Chart metadata linter.RunLinterRule(support.ErrorSev, chartFileName, validateChartAPIVersion(chartFile)) @@ -82,13 +81,6 @@ func validateChartName(cf *chart.Metadata) error { return nil } -func validateChartNameDirMatch(chartDir string, cf *chart.Metadata) error { - if cf.Name != filepath.Base(chartDir) { - return errors.Errorf("directory name (%s) and chart name (%s) must be the same", filepath.Base(chartDir), cf.Name) - } - return nil -} - func validateChartAPIVersion(cf *chart.Metadata) error { if cf.APIVersion == "" { return errors.New("apiVersion is required. The value must be either \"v1\" or \"v2\"") diff --git a/pkg/lint/rules/chartfile_test.go b/pkg/lint/rules/chartfile_test.go index ff2a63583..d032dd12f 100644 --- a/pkg/lint/rules/chartfile_test.go +++ b/pkg/lint/rules/chartfile_test.go @@ -30,18 +30,15 @@ import ( ) const ( - badChartDir = "testdata/badchartfile" - goodChartDir = "testdata/goodone" + badChartDir = "testdata/badchartfile" ) var ( badChartFilePath = filepath.Join(badChartDir, "Chart.yaml") - goodChartFilePath = filepath.Join(goodChartDir, "Chart.yaml") nonExistingChartFilePath = filepath.Join(os.TempDir(), "Chart.yaml") ) var badChart, _ = chartutil.LoadChartfile(badChartFilePath) -var goodChart, _ = chartutil.LoadChartfile(goodChartFilePath) // Validation functions Test func TestValidateChartYamlNotDirectory(t *testing.T) { @@ -73,24 +70,6 @@ func TestValidateChartName(t *testing.T) { } } -func TestValidateChartNameDirMatch(t *testing.T) { - err := validateChartNameDirMatch(goodChartDir, goodChart) - if err != nil { - t.Errorf("validateChartNameDirMatch to return no error, gor a linter error") - } - // It has not name - err = validateChartNameDirMatch(badChartDir, badChart) - if err == nil { - t.Errorf("validatechartnamedirmatch to return a linter error, got no error") - } - - // Wrong path - err = validateChartNameDirMatch(badChartDir, goodChart) - if err == nil { - t.Errorf("validatechartnamedirmatch to return a linter error, got no error") - } -} - func TestValidateChartVersion(t *testing.T) { var failTest = []struct { Version string @@ -209,36 +188,32 @@ func TestChartfile(t *testing.T) { Chartfile(&linter) msgs := linter.Messages - if len(msgs) != 7 { - t.Errorf("Expected 7 errors, got %d", len(msgs)) + if len(msgs) != 6 { + t.Errorf("Expected 6 errors, got %d", len(msgs)) } if !strings.Contains(msgs[0].Err.Error(), "name is required") { t.Errorf("Unexpected message 0: %s", msgs[0].Err) } - if !strings.Contains(msgs[1].Err.Error(), "directory name (badchartfile) and chart name () must be the same") { + if !strings.Contains(msgs[1].Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { t.Errorf("Unexpected message 1: %s", msgs[1].Err) } - if !strings.Contains(msgs[2].Err.Error(), "apiVersion is required. The value must be either \"v1\" or \"v2\"") { + if !strings.Contains(msgs[2].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { t.Errorf("Unexpected message 2: %s", msgs[2].Err) } - if !strings.Contains(msgs[3].Err.Error(), "version '0.0.0.0' is not a valid SemVer") { + if !strings.Contains(msgs[3].Err.Error(), "icon is recommended") { t.Errorf("Unexpected message 3: %s", msgs[3].Err) } - if !strings.Contains(msgs[4].Err.Error(), "icon is recommended") { + if !strings.Contains(msgs[4].Err.Error(), "chart type is not valid in apiVersion") { t.Errorf("Unexpected message 4: %s", msgs[4].Err) } - if !strings.Contains(msgs[5].Err.Error(), "chart type is not valid in apiVersion") { + if !strings.Contains(msgs[5].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { t.Errorf("Unexpected message 5: %s", msgs[5].Err) } - if !strings.Contains(msgs[6].Err.Error(), "dependencies are not valid in the Chart file with apiVersion") { - t.Errorf("Unexpected message 6: %s", msgs[6].Err) - } - } diff --git a/pkg/provenance/sign.go b/pkg/provenance/sign.go index 6032eb063..5d16779f1 100644 --- a/pkg/provenance/sign.go +++ b/pkg/provenance/sign.go @@ -169,7 +169,7 @@ func (s *Signatory) DecryptKey(fn PassphraseFetcher) error { if s.Entity == nil { return errors.New("private key not found") } else if s.Entity.PrivateKey == nil { - return errors.New("provided key is not a private key") + return errors.New("provided key is not a private key. Try providing a keyring with secret keys") } // Nothing else to do if key is not encrypted. @@ -203,7 +203,7 @@ func (s *Signatory) ClearSign(chartpath string) (string, error) { if s.Entity == nil { return "", errors.New("private key not found") } else if s.Entity.PrivateKey == nil { - return "", errors.New("provided key is not a private key") + return "", errors.New("provided key is not a private key. Try providing a keyring with secret keys") } if fi, err := os.Stat(chartpath); err != nil { diff --git a/pkg/strvals/parser.go b/pkg/strvals/parser.go index eaadbfb07..03adbd3cb 100644 --- a/pkg/strvals/parser.go +++ b/pkg/strvals/parser.go @@ -84,7 +84,7 @@ func ParseFile(s string, reader RunesValueReader) (map[string]interface{}, error return vals, err } -// ParseIntoString parses a strvals line nad merges the result into dest. +// ParseIntoString parses a strvals line and merges the result into dest. // // This method always returns a string as the value. func ParseIntoString(s string, dest map[string]interface{}) error { @@ -108,6 +108,9 @@ type RunesValueReader func([]rune) (interface{}, error) // parser is a simple parser that takes a strvals line and parses it into a // map representation. +// +// where sc is the source of the original data being parsed +// where data is the final parsed data from the parses with correct types type parser struct { sc *bytes.Buffer data map[string]interface{} @@ -285,7 +288,13 @@ func (t *parser) listItem(list []interface{}, i int) ([]interface{}, error) { // We have a nested object. Send to t.key inner := map[string]interface{}{} if len(list) > i { - inner = list[i].(map[string]interface{}) + var ok bool + inner, ok = list[i].(map[string]interface{}) + if !ok { + // We have indices out of order. Initialize empty value. + list[i] = map[string]interface{}{} + inner = list[i].(map[string]interface{}) + } } // Recurse @@ -367,6 +376,11 @@ func inMap(k rune, m map[rune]bool) bool { func typedVal(v []rune, st bool) interface{} { val := string(v) + + if st { + return val + } + if strings.EqualFold(val, "true") { return true } @@ -375,8 +389,16 @@ func typedVal(v []rune, st bool) interface{} { return false } - // If this value does not start with zero, and not returnString, try parsing it to an int - if !st && len(val) != 0 && val[0] != '0' { + if strings.EqualFold(val, "null") { + return nil + } + + if strings.EqualFold(val, "0") { + return int64(0) + } + + // If this value does not start with zero, try parsing it to an int + if len(val) != 0 && val[0] != '0' { if iv, err := strconv.ParseInt(val, 10, 64); err == nil { return iv } diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index ff8d58587..7f38efa52 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -75,12 +75,32 @@ func TestParseSet(t *testing.T) { expect: map[string]interface{}{"long_int_string": "1234567890"}, err: false, }, + { + str: "boolean=true", + expect: map[string]interface{}{"boolean": "true"}, + err: false, + }, + { + str: "is_null=null", + expect: map[string]interface{}{"is_null": "null"}, + err: false, + }, + { + str: "zero=0", + expect: map[string]interface{}{"zero": "0"}, + err: false, + }, } tests := []struct { str string expect map[string]interface{} err bool }{ + { + "name1=null,f=false,t=true", + map[string]interface{}{"name1": nil, "f": false, "t": true}, + false, + }, { "name1=value1", map[string]interface{}{"name1": "value1"}, @@ -108,10 +128,23 @@ func TestParseSet(t *testing.T) { str: "leading_zeros=00009", expect: map[string]interface{}{"leading_zeros": "00009"}, }, + { + str: "zero_int=0", + expect: map[string]interface{}{"zero_int": 0}, + }, { str: "long_int=1234567890", expect: map[string]interface{}{"long_int": 1234567890}, }, + { + str: "boolean=true", + expect: map[string]interface{}{"boolean": true}, + }, + { + str: "is_null=null", + expect: map[string]interface{}{"is_null": nil}, + err: false, + }, { str: "name1,name2=", err: true, @@ -270,6 +303,30 @@ func TestParseSet(t *testing.T) { str: "nested[1][1]=1", expect: map[string]interface{}{"nested": []interface{}{nil, []interface{}{nil, 1}}}, }, + { + str: "name1.name2[0].foo=bar,name1.name2[1].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{{"foo": "bar"}, {"foo": "bar"}}, + }, + }, + }, + { + str: "name1.name2[1].foo=bar,name1.name2[0].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{{"foo": "bar"}, {"foo": "bar"}}, + }, + }, + }, + { + str: "name1.name2[1].foo=bar", + expect: map[string]interface{}{ + "name1": map[string]interface{}{ + "name2": []map[string]interface{}{nil, {"foo": "bar"}}, + }, + }, + }, } for _, tt := range tests { @@ -331,12 +388,13 @@ func TestParseInto(t *testing.T) { "inner2": "value2", }, } - input := "outer.inner1=value1,outer.inner3=value3" + input := "outer.inner1=value1,outer.inner3=value3,outer.inner4=4" expect := map[string]interface{}{ "outer": map[string]interface{}{ "inner1": "value1", "inner2": "value2", "inner3": "value3", + "inner4": 4, }, } @@ -357,6 +415,39 @@ func TestParseInto(t *testing.T) { t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) } } +func TestParseIntoString(t *testing.T) { + got := map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": "overwrite", + "inner2": "value2", + }, + } + input := "outer.inner1=1,outer.inner3=3" + expect := map[string]interface{}{ + "outer": map[string]interface{}{ + "inner1": "1", + "inner2": "value2", + "inner3": "3", + }, + } + + if err := ParseIntoString(input, got); err != nil { + t.Fatal(err) + } + + y1, err := yaml.Marshal(expect) + if err != nil { + t.Fatal(err) + } + y2, err := yaml.Marshal(got) + if err != nil { + t.Fatalf("Error serializing parsed value: %s", err) + } + + if string(y1) != string(y2) { + t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + } +} func TestParseIntoFile(t *testing.T) { got := map[string]interface{}{} @@ -367,7 +458,7 @@ func TestParseIntoFile(t *testing.T) { rs2v := func(rs []rune) (interface{}, error) { v := string(rs) if v != "path1" { - t.Errorf("%s: RunesValueReader: Expected value path1, got %s", input, v) + t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v) return "", nil } return "value1", nil diff --git a/scripts/get b/scripts/get index 3f645f807..711635ee3 100755 --- a/scripts/get +++ b/scripts/get @@ -78,13 +78,16 @@ verifySupported() { # checkDesiredVersion checks if the desired version is available. checkDesiredVersion() { if [ "x$DESIRED_VERSION" == "x" ]; then + # FIXME(bacongobbler): hard code the desired version for the time being. + # A better fix would be to filter for Helm 2 release pages. + TAG="v2.16.1" # Get tag from release URL - local latest_release_url="https://github.com/helm/helm/releases/latest" - if type "curl" > /dev/null; then - TAG=$(curl -Ls -o /dev/null -w %{url_effective} $latest_release_url | grep -oE "[^/]+$" ) - elif type "wget" > /dev/null; then - TAG=$(wget $latest_release_url --server-response -O /dev/null 2>&1 | awk '/^ Location: /{DEST=$2} END{ print DEST}' | grep -oE "[^/]+$") - fi + # local latest_release_url="https://github.com/helm/helm/releases/latest" + # if type "curl" > /dev/null; then + # TAG=$(curl -Ls -o /dev/null -w %{url_effective} $latest_release_url | grep -oE "[^/]+$" ) + # elif type "wget" > /dev/null; then + # TAG=$(wget $latest_release_url --server-response -O /dev/null 2>&1 | awk '/^ Location: /{DEST=$2} END{ print DEST}' | grep -oE "[^/]+$") + # fi else TAG=$DESIRED_VERSION fi @@ -187,7 +190,7 @@ testVersion() { help () { echo "Accepted cli arguments are:" echo -e "\t[--help|-h ] ->> prints this help" - echo -e "\t[--version|-v ] . When not defined it defaults to latest" + echo -e "\t[--version|-v ]" echo -e "\te.g. --version v2.4.0 or -v latest" echo -e "\t[--no-sudo] ->> install without sudo" } diff --git a/scripts/get-helm-3 b/scripts/get-helm-3 new file mode 100755 index 000000000..c1655a68e --- /dev/null +++ b/scripts/get-helm-3 @@ -0,0 +1,236 @@ +#!/usr/bin/env bash + +# Copyright The Helm Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The install script is based off of the MIT-licensed script from glide, +# the package manager for Go: https://github.com/Masterminds/glide.sh/blob/master/get + +PROJECT_NAME="helm" + +: ${USE_SUDO:="true"} +: ${HELM_INSTALL_DIR:="/usr/local/bin"} + +# initArch discovers the architecture for this system. +initArch() { + ARCH=$(uname -m) + case $ARCH in + armv5*) ARCH="armv5";; + armv6*) ARCH="armv6";; + armv7*) ARCH="arm";; + aarch64) ARCH="arm64";; + x86) ARCH="386";; + x86_64) ARCH="amd64";; + i686) ARCH="386";; + i386) ARCH="386";; + esac +} + +# initOS discovers the operating system for this system. +initOS() { + OS=$(echo `uname`|tr '[:upper:]' '[:lower:]') + + case "$OS" in + # Minimalist GNU for Windows + mingw*) OS='windows';; + esac +} + +# runs the given command as root (detects if we are root already) +runAsRoot() { + local CMD="$*" + + if [ $EUID -ne 0 -a $USE_SUDO = "true" ]; then + CMD="sudo $CMD" + fi + + $CMD +} + +# verifySupported checks that the os/arch combination is supported for +# binary builds. +verifySupported() { + local supported="darwin-386\ndarwin-amd64\nlinux-386\nlinux-amd64\nlinux-arm\nlinux-arm64\nlinux-ppc64le\nwindows-386\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" + exit 1 + fi + + if ! type "curl" > /dev/null && ! type "wget" > /dev/null; then + echo "Either curl or wget is required" + exit 1 + fi +} + +# checkDesiredVersion checks if the desired version is available. +checkDesiredVersion() { + if [ "x$DESIRED_VERSION" == "x" ]; then + # Get tag from release URL + local latest_release_url="https://github.com/helm/helm/releases/latest" + if type "curl" > /dev/null; then + TAG=$(curl -Ls -o /dev/null -w %{url_effective} $latest_release_url | grep -oE "[^/]+$" ) + elif type "wget" > /dev/null; then + TAG=$(wget $latest_release_url --server-response -O /dev/null 2>&1 | awk '/^ Location: /{DEST=$2} END{ print DEST}' | grep -oE "[^/]+$") + fi + else + TAG=$DESIRED_VERSION + fi +} + +# checkHelmInstalledVersion checks which version of helm is installed and +# if it needs to be changed. +checkHelmInstalledVersion() { + if [[ -f "${HELM_INSTALL_DIR}/${PROJECT_NAME}" ]]; then + local version=$("${HELM_INSTALL_DIR}/${PROJECT_NAME}" version --template="{{ .Version }}") + if [[ "$version" == "$TAG" ]]; then + echo "Helm ${version} is already ${DESIRED_VERSION:-latest}" + return 0 + else + echo "Helm ${TAG} is available. Changing from version ${version}." + return 1 + fi + else + return 1 + fi +} + +# downloadFile downloads the latest binary package and also the checksum +# for that binary. +downloadFile() { + HELM_DIST="helm-$TAG-$OS-$ARCH.tar.gz" + DOWNLOAD_URL="https://get.helm.sh/$HELM_DIST" + CHECKSUM_URL="$DOWNLOAD_URL.sha256" + HELM_TMP_ROOT="$(mktemp -dt helm-installer-XXXXXX)" + HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST" + HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256" + echo "Downloading $DOWNLOAD_URL" + if type "curl" > /dev/null; then + curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE" + elif type "wget" > /dev/null; then + wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL" + fi + if type "curl" > /dev/null; then + curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE" + elif type "wget" > /dev/null; then + wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL" + fi +} + +# installFile verifies the SHA256 for the file, then unpacks and +# installs it. +installFile() { + HELM_TMP="$HELM_TMP_ROOT/$PROJECT_NAME" + local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}') + local expected_sum=$(cat ${HELM_SUM_FILE}) + if [ "$sum" != "$expected_sum" ]; then + echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting." + exit 1 + fi + + mkdir -p "$HELM_TMP" + tar xf "$HELM_TMP_FILE" -C "$HELM_TMP" + HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/$PROJECT_NAME" + echo "Preparing to install $PROJECT_NAME into ${HELM_INSTALL_DIR}" + runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR" + echo "$PROJECT_NAME installed into $HELM_INSTALL_DIR/$PROJECT_NAME" +} + +# fail_trap is executed if an error occurs. +fail_trap() { + result=$? + if [ "$result" != "0" ]; then + if [[ -n "$INPUT_ARGUMENTS" ]]; then + echo "Failed to install $PROJECT_NAME with the arguments provided: $INPUT_ARGUMENTS" + help + else + echo "Failed to install $PROJECT_NAME" + fi + echo -e "\tFor support, go to https://github.com/helm/helm." + fi + cleanup + exit $result +} + +# testVersion tests the installed client to make sure it is working. +testVersion() { + set +e + HELM="$(which $PROJECT_NAME)" + if [ "$?" = "1" ]; then + echo "$PROJECT_NAME not found. Is $HELM_INSTALL_DIR on your "'$PATH?' + exit 1 + fi + set -e +} + +# help provides possible cli installation arguments +help () { + echo "Accepted cli arguments are:" + echo -e "\t[--help|-h ] ->> prints this help" + echo -e "\t[--version|-v ] . When not defined it fetches the latest release from GitHub" + echo -e "\te.g. --version v3.0.0 or -v canary" + echo -e "\t[--no-sudo] ->> install without sudo" +} + +# cleanup temporary files to avoid https://github.com/helm/helm/issues/2977 +cleanup() { + if [[ -d "${HELM_TMP_ROOT:-}" ]]; then + rm -rf "$HELM_TMP_ROOT" + fi +} + +# Execution + +#Stop execution on any error +trap "fail_trap" EXIT +set -e + +# Parsing input arguments (if any) +export INPUT_ARGUMENTS="${@}" +set -u +while [[ $# -gt 0 ]]; do + case $1 in + '--version'|-v) + shift + if [[ $# -ne 0 ]]; then + export DESIRED_VERSION="${1}" + else + echo -e "Please provide the desired version. e.g. --version v3.0.0 or -v canary" + exit 0 + fi + ;; + '--no-sudo') + USE_SUDO="false" + ;; + '--help'|-h) + help + exit 0 + ;; + *) exit 1 + ;; + esac + shift +done +set +u + +initArch +initOS +verifySupported +checkDesiredVersion +if ! checkHelmInstalledVersion; then + downloadFile + installFile +fi +testVersion +cleanup diff --git a/scripts/validate-license.sh b/scripts/validate-license.sh index 00bd38ea2..dc247436f 100755 --- a/scripts/validate-license.sh +++ b/scripts/validate-license.sh @@ -27,14 +27,16 @@ find_files() { \( -name '*.go' -o -name '*.sh' \) } -mapfile -t failed_license_header < <(find_files | xargs grep -L 'Licensed under the Apache License, Version 2.0 (the "License")') +# Use "|| :" to ignore the error code when grep returns empty +failed_license_header=($(find_files | xargs grep -L 'Licensed under the Apache License, Version 2.0 (the "License")' || :)) if (( ${#failed_license_header[@]} > 0 )); then echo "Some source files are missing license headers." printf '%s\n' "${failed_license_header[@]}" exit 1 fi -mapfile -t failed_copyright_header < <(find_files | xargs grep -L 'Copyright The Helm Authors.') +# Use "|| :" to ignore the error code when grep returns empty +failed_copyright_header=($(find_files | xargs grep -L 'Copyright The Helm Authors.' || :)) if (( ${#failed_copyright_header[@]} > 0 )); then echo "Some source files are missing the copyright header." printf '%s\n' "${failed_copyright_header[@]}"