Merge pull request #1 from helm/master

update
pull/8813/head
Gabb-c 5 years ago committed by GitHub
commit 7f8a9652e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,7 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"

@ -232,8 +232,9 @@ Like any good open source project, we use Pull Requests (PRs) to track code chan
3. Assigning reviews 3. Assigning reviews
- Once a review has the `awaiting review` label, maintainers will review them as schedule - Once a review has the `awaiting review` label, maintainers will review them as schedule
permits. The maintainer who takes the issue should self-request a review. permits. The maintainer who takes the issue should self-request a review.
- Any PR with the `size/large` label requires 2 review approvals from maintainers before it can - PRs from a community member with the label `size/S` or larger requires 2 review approvals from
be merged. Those with `size/medium` or `size/small` are per the judgement of the maintainers. maintainers before it can be merged. Those with `size/XS` are per the judgement of the
maintainers.
4. Reviewing/Discussion 4. Reviewing/Discussion
- All reviews will be completed using Github review tool. - All reviews will be completed using Github review tool.
- A "Comment" review should be used when there are questions about the code that should be - A "Comment" review should be used when there are questions about the code that should be
@ -313,15 +314,16 @@ makes 30 lines of changes in 1 file, but it changes key functionality, it will l
feature, but requires another 150 lines of tests to cover all cases, could be labeled as `size/S` feature, but requires another 150 lines of tests to cover all cases, could be labeled as `size/S`
even though the number of lines is greater than defined below. even though the number of lines is greater than defined below.
PRs submitted by a core maintainer, regardless of size, only requires approval from one additional Any changes from the community labeled as `size/S` or larger should be thoroughly tested before
maintainer. This ensures there are at least two maintainers who are aware of any significant PRs merging and always requires approval from 2 core maintainers. PRs submitted by a core maintainer,
introduced to the codebase. regardless of size, only requires approval from one additional maintainer. This ensures there are at
least two maintainers who are aware of any significant PRs introduced to the codebase.
| Label | Description | | Label | Description |
| ----- | ----------- | | ----- | ----------- |
| `size/XS` | Denotes a PR that changes 0-9 lines, ignoring generated files. Very little testing may be required depending on the change. | | `size/XS` | Denotes a PR that changes 0-9 lines, ignoring generated files. Very little testing may be required depending on the change. |
| `size/S` | Denotes a PR that changes 10-29 lines, ignoring generated files. Only small amounts of manual testing may be required. | | `size/S` | Denotes a PR that changes 10-29 lines, ignoring generated files. Only small amounts of manual testing may be required. |
| `size/M` | Denotes a PR that changes 30-99 lines, ignoring generated files. Manual validation should be required. | | `size/M` | Denotes a PR that changes 30-99 lines, ignoring generated files. Manual validation should be required. |
| `size/L` | Denotes a PR that changes 100-499 lines, ignoring generated files. This should be thoroughly tested before merging and always requires 2 approvals. | | `size/L` | Denotes a PR that changes 100-499 lines, ignoring generated files. |
| `size/XL` | Denotes a PR that changes 500-999 lines, ignoring generated files. This should be thoroughly tested before merging and always requires 2 approvals. | | `size/XL` | Denotes a PR that changes 500-999 lines, ignoring generated files. |
| `size/XXL` | Denotes a PR that changes 1000+ lines, ignoring generated files. This should be thoroughly tested before merging and always requires 2 approvals. | | `size/XXL` | Denotes a PR that changes 1000+ lines, ignoring generated files. |

@ -1,4 +1,5 @@
BINDIR := $(CURDIR)/bin BINDIR := $(CURDIR)/bin
INSTALL_PATH ?= /usr/local/bin
DIST_DIRS := find * -type d -exec DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le linux/s390x windows/amd64 TARGETS := darwin/amd64 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 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 TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 darwin-amd64.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
@ -49,6 +50,7 @@ endif
LDFLAGS += -X helm.sh/helm/v3/internal/version.metadata=${VERSION_METADATA} LDFLAGS += -X helm.sh/helm/v3/internal/version.metadata=${VERSION_METADATA}
LDFLAGS += -X helm.sh/helm/v3/internal/version.gitCommit=${GIT_COMMIT} LDFLAGS += -X helm.sh/helm/v3/internal/version.gitCommit=${GIT_COMMIT}
LDFLAGS += -X helm.sh/helm/v3/internal/version.gitTreeState=${GIT_DIRTY} LDFLAGS += -X helm.sh/helm/v3/internal/version.gitTreeState=${GIT_DIRTY}
LDFLAGS += $(EXT_LDFLAGS)
.PHONY: all .PHONY: all
all: build all: build
@ -62,6 +64,13 @@ build: $(BINDIR)/$(BINNAME)
$(BINDIR)/$(BINNAME): $(SRC) $(BINDIR)/$(BINNAME): $(SRC)
GO111MODULE=on go build $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm GO111MODULE=on go build $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o '$(BINDIR)'/$(BINNAME) ./cmd/helm
# ------------------------------------------------------------------------------
# install
.PHONY: install
install: build
@install "$(BINDIR)/$(BINNAME)" "$(INSTALL_PATH)/$(BINNAME)"
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# test # test

@ -107,6 +107,7 @@ func (o *createOptions) run(out io.Writer) error {
return chartutil.CreateFrom(cfile, filepath.Dir(o.name), lstarter) return chartutil.CreateFrom(cfile, filepath.Dir(o.name), lstarter)
} }
chartutil.Stderr = out
_, err := chartutil.Create(chartname, filepath.Dir(o.name)) _, err := chartutil.Create(chartname, filepath.Dir(o.name))
return err return err
} }

@ -28,7 +28,7 @@ import (
) )
func TestDependencyBuildCmd(t *testing.T) { func TestDependencyBuildCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz")
defer srv.Stop() defer srv.Stop()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

@ -33,7 +33,7 @@ import (
) )
func TestDependencyUpdateCmd(t *testing.T) { func TestDependencyUpdateCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -121,7 +121,7 @@ func TestDependencyUpdateCmd_DontDeleteOldChartsOnError(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() defer ensure.HelmHome(t)()
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -69,7 +69,7 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
formatNames = append(formatNames, format) formatNames = append(formatNames, format)
} }
} }
return formatNames, cobra.ShellCompDirectiveDefault return formatNames, cobra.ShellCompDirectiveNoFileComp
}) })
if err != nil { if err != nil {

@ -59,7 +59,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
found, err := plugin.FindPlugins(settings.PluginsDirectory) found, err := plugin.FindPlugins(settings.PluginsDirectory)
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, "failed to load plugins: %s", err) fmt.Fprintf(os.Stderr, "failed to load plugins: %s\n", err)
return return
} }
@ -154,7 +154,7 @@ func callPluginExecutable(pluginName string, main string, argv []string, out io.
func manuallyProcessArgs(args []string) ([]string, []string) { func manuallyProcessArgs(args []string) ([]string, []string) {
known := []string{} known := []string{}
unknown := []string{} unknown := []string{}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--registry-config", "--repository-cache", "--repository-config"} kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--kube-as-user", "--kube-as-group", "--registry-config", "--repository-cache", "--repository-config"}
knownArg := func(a string) bool { knownArg := func(a string) bool {
for _, pre := range kvargs { for _, pre := range kvargs {
if strings.HasPrefix(a, pre+"=") { if strings.HasPrefix(a, pre+"=") {

@ -114,6 +114,7 @@ func newPackageCmd(out io.Writer) *cobra.Command {
f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package") f.BoolVar(&client.Sign, "sign", false, "use a PGP private key to sign this package")
f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true") f.StringVar(&client.Key, "key", "", "name of the key to use when signing. Used if --sign is true")
f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring") f.StringVar(&client.Keyring, "keyring", defaultKeyring(), "location of a public keyring")
f.StringVar(&client.PassphraseFile, "passphrase-file", "", `location of a file which contains the passphrase for the signing key. Use "-" in order to read from stdin.`)
f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version") f.StringVar(&client.Version, "version", "", "set the version on the chart to this semver version")
f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version") f.StringVar(&client.AppVersion, "app-version", "", "set the appVersion on the chart to this version")
f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.") f.StringVarP(&client.Destination, "destination", "d", ".", "location to write the chart.")

@ -19,6 +19,7 @@ import (
"fmt" "fmt"
"io" "io"
"github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/cmd/helm/require"
@ -81,7 +82,7 @@ func (o *pluginInstallOptions) run(out io.Writer) error {
debug("loading plugin from %s", i.Path()) debug("loading plugin from %s", i.Path())
p, err := plugin.LoadDir(i.Path()) p, err := plugin.LoadDir(i.Path())
if err != nil { if err != nil {
return err return errors.Wrap(err, "plugin is installed but unusable")
} }
if err := runHook(p, plugin.Install); err != nil { if err := runHook(p, plugin.Install); err != nil {

@ -37,6 +37,9 @@ func TestManuallyProcessArgs(t *testing.T) {
"--kubeconfig", "/home/foo", "--kubeconfig", "/home/foo",
"--kube-context=test1", "--kube-context=test1",
"--kube-context", "test1", "--kube-context", "test1",
"--kube-as-user", "pikachu",
"--kube-as-group", "teatime",
"--kube-as-group", "admins",
"-n=test2", "-n=test2",
"-n", "test2", "-n", "test2",
"--namespace=test2", "--namespace=test2",
@ -51,6 +54,9 @@ func TestManuallyProcessArgs(t *testing.T) {
"--kubeconfig", "/home/foo", "--kubeconfig", "/home/foo",
"--kube-context=test1", "--kube-context=test1",
"--kube-context", "test1", "--kube-context", "test1",
"--kube-as-user", "pikachu",
"--kube-as-group", "teatime",
"--kube-as-group", "admins",
"-n=test2", "-n=test2",
"-n", "test2", "-n", "test2",
"--namespace=test2", "--namespace=test2",

@ -26,7 +26,7 @@ import (
) )
func TestPullCmd(t *testing.T) { func TestPullCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -38,11 +38,11 @@ import (
) )
type repoAddOptions struct { type repoAddOptions struct {
name string name string
url string url string
username string username string
password string password string
noUpdate bool forceUpdate bool
certFile string certFile string
keyFile string keyFile string
@ -51,6 +51,9 @@ type repoAddOptions struct {
repoFile string repoFile string
repoCache string repoCache string
// Deprecated, but cannot be removed until Helm 4
deprecatedNoUpdate bool
} }
func newRepoAddCmd(out io.Writer) *cobra.Command { func newRepoAddCmd(out io.Writer) *cobra.Command {
@ -74,7 +77,8 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&o.username, "username", "", "chart repository username") f.StringVar(&o.username, "username", "", "chart repository username")
f.StringVar(&o.password, "password", "", "chart repository password") f.StringVar(&o.password, "password", "", "chart repository password")
f.BoolVar(&o.noUpdate, "no-update", false, "raise error if repo is already registered") f.BoolVar(&o.forceUpdate, "force-update", false, "replace (overwrite) the repo if it already exists")
f.BoolVar(&o.deprecatedNoUpdate, "no-update", false, "Ignored. Formerly, it would disabled forced updates. It is deprecated by force-update.")
f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") f.StringVar(&o.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file")
f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") f.StringVar(&o.keyFile, "key-file", "", "identify HTTPS client using this SSL key file")
f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") f.StringVar(&o.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
@ -112,10 +116,6 @@ func (o *repoAddOptions) run(out io.Writer) error {
return err return err
} }
if o.noUpdate && f.Has(o.name) {
return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name)
}
if o.username != "" && o.password == "" { if o.username != "" && o.password == "" {
fd := int(os.Stdin.Fd()) fd := int(os.Stdin.Fd())
fmt.Fprint(out, "Password: ") fmt.Fprint(out, "Password: ")
@ -138,6 +138,23 @@ func (o *repoAddOptions) run(out io.Writer) error {
InsecureSkipTLSverify: o.insecureSkipTLSverify, InsecureSkipTLSverify: o.insecureSkipTLSverify,
} }
// If the repo exists do one of two things:
// 1. If the configuration for the name is the same continue without error
// 2. When the config is different require --force-update
if !o.forceUpdate && f.Has(o.name) {
existing := f.Get(o.name)
if c != *existing {
// The input coming in for the name is different from what is already
// configured. Return an error.
return errors.Errorf("repository name (%s) already exists, please specify a different name", o.name)
}
// The add is idempotent so do nothing
fmt.Fprintf(out, "%q already exists with the same configuration, skipping\n", o.name)
return nil
}
r, err := repo.NewChartRepository(&c, getter.All(settings)) r, err := repo.NewChartRepository(&c, getter.All(settings))
if err != nil { if err != nil {
return err return err

@ -34,26 +34,50 @@ import (
) )
func TestRepoAddCmd(t *testing.T) { func TestRepoAddCmd(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testserver/*.*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer srv.Stop() defer srv.Stop()
// A second test server is setup to verify URL changing
srv2, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil {
t.Fatal(err)
}
defer srv2.Stop()
tmpdir := ensure.TempDir(t) tmpdir := ensure.TempDir(t)
repoFile := filepath.Join(tmpdir, "repositories.yaml") repoFile := filepath.Join(tmpdir, "repositories.yaml")
tests := []cmdTestCase{{ tests := []cmdTestCase{
name: "add a repository", {
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir), name: "add a repository",
golden: "output/repo-add.txt", cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir),
}} golden: "output/repo-add.txt",
},
{
name: "add repository second time",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv.URL(), repoFile, tmpdir),
golden: "output/repo-add2.txt",
},
{
name: "add repository different url",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s", srv2.URL(), repoFile, tmpdir),
wantError: true,
},
{
name: "add repository second time",
cmd: fmt.Sprintf("repo add test-name %s --repository-config %s --repository-cache %s --force-update", srv2.URL(), repoFile, tmpdir),
golden: "output/repo-add.txt",
},
}
runTestCmd(t, tests) runTestCmd(t, tests)
} }
func TestRepoAdd(t *testing.T) { func TestRepoAdd(t *testing.T) {
ts, err := repotest.NewTempServer("testdata/testserver/*.*") ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -65,10 +89,11 @@ func TestRepoAdd(t *testing.T) {
const testRepoName = "test-name" const testRepoName = "test-name"
o := &repoAddOptions{ o := &repoAddOptions{
name: testRepoName, name: testRepoName,
url: ts.URL(), url: ts.URL(),
noUpdate: true, forceUpdate: false,
repoFile: repoFile, deprecatedNoUpdate: true,
repoFile: repoFile,
} }
os.Setenv(xdg.CacheHomeEnvVar, rootDir) os.Setenv(xdg.CacheHomeEnvVar, rootDir)
@ -94,7 +119,7 @@ func TestRepoAdd(t *testing.T) {
t.Errorf("Error cache charts file was not created for repository %s", testRepoName) t.Errorf("Error cache charts file was not created for repository %s", testRepoName)
} }
o.noUpdate = false o.forceUpdate = true
if err := o.run(ioutil.Discard); err != nil { if err := o.run(ioutil.Discard); err != nil {
t.Errorf("Repository was not updated: %s", err) t.Errorf("Repository was not updated: %s", err)
@ -118,7 +143,7 @@ func TestRepoAddConcurrentDirNotExist(t *testing.T) {
} }
func repoAddConcurrent(t *testing.T, testName, repoFile string) { func repoAddConcurrent(t *testing.T, testName, repoFile string) {
ts, err := repotest.NewTempServer("testdata/testserver/*.*") ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -130,10 +155,11 @@ func repoAddConcurrent(t *testing.T, testName, repoFile string) {
go func(name string) { go func(name string) {
defer wg.Done() defer wg.Done()
o := &repoAddOptions{ o := &repoAddOptions{
name: name, name: name,
url: ts.URL(), url: ts.URL(),
noUpdate: true, deprecatedNoUpdate: true,
repoFile: repoFile, forceUpdate: false,
repoFile: repoFile,
} }
if err := o.run(ioutil.Discard); err != nil { if err := o.run(ioutil.Discard); err != nil {
t.Error(err) t.Error(err)

@ -30,7 +30,7 @@ import (
) )
func TestRepoRemove(t *testing.T) { func TestRepoRemove(t *testing.T) {
ts, err := repotest.NewTempServer("testdata/testserver/*.*") ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -75,7 +75,7 @@ func TestUpdateCharts(t *testing.T) {
defer resetEnv()() defer resetEnv()()
defer ensure.HelmHome(t)() defer ensure.HelmHome(t)()
ts, err := repotest.NewTempServer("testdata/testserver/*.*") ts, err := repotest.NewTempServerWithCleanup(t, "testdata/testserver/*.*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -48,11 +48,22 @@ Environment variables:
| $HELM_CACHE_HOME | set an alternative location for storing cached files. | | $HELM_CACHE_HOME | set an alternative location for storing cached files. |
| $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. | | $HELM_CONFIG_HOME | set an alternative location for storing Helm configuration. |
| $HELM_DATA_HOME | set an alternative location for storing Helm data. | | $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, postgres | | $HELM_DRIVER | set the backend storage driver. Values are: configmap, secret, memory, postgres |
| $HELM_DRIVER_SQL_CONNECTION_STRING | set the connection string the SQL storage driver should use. | | $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_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_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") | | $KUBECONFIG | set an alternative Kubernetes configuration file (default "~/.kube/config") |
| $HELM_KUBEAPISERVER | set the Kubernetes API Server Endpoint for authentication |
| $HELM_KUBEASGROUPS | set the Username to impersonate for the operation. |
| $HELM_KUBEASUSER | set the Groups to use for impoersonation using a comma-separated list. |
| $HELM_KUBECONTEXT | set the name of the kubeconfig context. |
| $HELM_KUBETOKEN | set the Bearer KubeToken used for authentication. |
Helm stores cache, configuration, and data based on the following configuration order: Helm stores cache, configuration, and data based on the following configuration order:
@ -193,5 +204,8 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Find and add plugins // Find and add plugins
loadPlugins(cmd, out) loadPlugins(cmd, out)
// Check permissions on critical files
checkPerms(out)
return cmd, nil return cmd, nil
} }

@ -0,0 +1,60 @@
// +build !windows
/*
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 main
import (
"fmt"
"io"
"os"
"os/user"
"path/filepath"
)
func checkPerms(out io.Writer) {
// This function MUST NOT FAIL, as it is just a check for a common permissions problem.
// If for some reason the function hits a stopping condition, it may panic. But only if
// we can be sure that it is panicing because Helm cannot proceed.
kc := settings.KubeConfig
if kc == "" {
kc = os.Getenv("KUBECONFIG")
}
if kc == "" {
u, err := user.Current()
if err != nil {
// No idea where to find KubeConfig, so return silently. Many helm commands
// can proceed happily without a KUBECONFIG, so this is not a fatal error.
return
}
kc = filepath.Join(u.HomeDir, ".kube", "config")
}
fi, err := os.Stat(kc)
if err != nil {
// DO NOT error if no KubeConfig is found. Not all commands require one.
return
}
perm := fi.Mode().Perm()
if perm&0040 > 0 {
fmt.Fprintf(out, "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location: %s\n", kc)
}
if perm&0004 > 0 {
fmt.Fprintf(out, "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location: %s\n", kc)
}
}

@ -0,0 +1,63 @@
// +build !windows
/*
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 main
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCheckPerms(t *testing.T) {
tdir, err := ioutil.TempDir("", "helmtest")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tdir)
tfile := filepath.Join(tdir, "testconfig")
fh, err := os.OpenFile(tfile, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0440)
if err != nil {
t.Errorf("Failed to create temp file: %s", err)
}
tconfig := settings.KubeConfig
settings.KubeConfig = tfile
defer func() { settings.KubeConfig = tconfig }()
var b bytes.Buffer
checkPerms(&b)
expectPrefix := "WARNING: Kubernetes configuration file is group-readable. This is insecure. Location:"
if !strings.HasPrefix(b.String(), expectPrefix) {
t.Errorf("Expected to get a warning for group perms. Got %q", b.String())
}
if err := fh.Chmod(0404); err != nil {
t.Errorf("Could not change mode on file: %s", err)
}
b.Reset()
checkPerms(&b)
expectPrefix = "WARNING: Kubernetes configuration file is world-readable. This is insecure. Location:"
if !strings.HasPrefix(b.String(), expectPrefix) {
t.Errorf("Expected to get a warning for world perms. Got %q", b.String())
}
}

@ -0,0 +1,24 @@
/*
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 main
import "io"
func checkPerms(out io.Writer) {
// Not yet implemented on Windows. If you know how to do a comprehensive perms
// check on Windows, contributions welcomed!
}

@ -26,7 +26,7 @@ import (
) )
func TestShowPreReleaseChart(t *testing.T) { func TestShowPreReleaseChart(t *testing.T) {
srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/testcharts/*.tgz*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -4,6 +4,8 @@ HELM_CONFIG_HOME
HELM_DATA_HOME HELM_DATA_HOME
HELM_DEBUG HELM_DEBUG
HELM_KUBEAPISERVER HELM_KUBEAPISERVER
HELM_KUBEASGROUPS
HELM_KUBEASUSER
HELM_KUBECONTEXT HELM_KUBECONTEXT
HELM_KUBETOKEN HELM_KUBETOKEN
HELM_MAX_HISTORY HELM_MAX_HISTORY

@ -1,5 +1,5 @@
table table
json json
yaml yaml
:0 :4
Completion ended with directive: ShellCompDirectiveDefault Completion ended with directive: ShellCompDirectiveNoFileComp

@ -0,0 +1 @@
"test-name" already exists with the same configuration, skipping

@ -58,14 +58,15 @@ var (
errMissingRelease = errors.New("no release provided") errMissingRelease = errors.New("no release provided")
// errInvalidRevision indicates that an invalid release revision number was provided. // errInvalidRevision indicates that an invalid release revision number was provided.
errInvalidRevision = errors.New("invalid release revision") errInvalidRevision = errors.New("invalid release revision")
// errInvalidName indicates that an invalid release name was provided
errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53")
// errPending indicates that another instance of Helm is already applying an operation on a release. // errPending indicates that another instance of Helm is already applying an operation on a release.
errPending = errors.New("another operation (install/upgrade/rollback) is in progress") errPending = errors.New("another operation (install/upgrade/rollback) is in progress")
) )
// ValidName is a regular expression for resource names. // ValidName is a regular expression for resource names.
// //
// DEPRECATED: This will be removed in Helm 4, and is no longer used here. See
// pkg/chartutil.ValidateName for the replacement.
//
// According to the Kubernetes help text, the regular expression it uses is: // According to the Kubernetes help text, the regular expression it uses is:
// //
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)* // [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
@ -294,7 +295,7 @@ func (c *Configuration) Now() time.Time {
} }
func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) { func (c *Configuration) releaseContent(name string, version int) (*release.Release, error) {
if err := validateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name) return nil, errors.Errorf("releaseContent: Release name is invalid: %s", name)
} }

@ -20,6 +20,7 @@ import (
"flag" "flag"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os"
"path/filepath" "path/filepath"
"testing" "testing"
@ -56,6 +57,8 @@ func actionConfigFixture(t *testing.T) *Configuration {
t.Fatal(err) t.Fatal(err)
} }
t.Cleanup(func() { os.RemoveAll(tdir) })
cache, err := registry.NewCache( cache, err := registry.NewCache(
registry.CacheOptDebug(true), registry.CacheOptDebug(true),
registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)), registry.CacheOptRoot(filepath.Join(tdir, registry.CacheRootDir)),
@ -316,40 +319,3 @@ func TestGetVersionSet(t *testing.T) {
t.Error("Non-existent version is reported found.") t.Error("Non-existent version is reported found.")
} }
} }
// TestValidName is a regression test for ValidName
//
// Kubernetes has strict naming conventions for resource names. This test represents
// those conventions.
//
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// NOTE: At the time of this writing, the docs above say that names cannot begin with
// digits. However, `kubectl`'s regular expression explicit allows this, and
// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
func TestValidName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
}
for input, expectPass := range names {
if ValidName.MatchString(input) != expectPass {
st := "fail"
if expectPass {
st = "succeed"
}
t.Errorf("Expected %q to %s", input, st)
}
}
}

@ -21,6 +21,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
@ -61,6 +62,7 @@ func (d *Dependency) List(chartpath string, out io.Writer) error {
return nil return nil
} }
// dependecyStatus returns a string describing the status of a dependency viz a viz the parent chart.
func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string { func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, parent *chart.Chart) string {
filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*") filename := fmt.Sprintf("%s-%s.tgz", dep.Name, "*")
@ -75,35 +77,40 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
case err != nil: case err != nil:
return "bad pattern" return "bad pattern"
case len(archives) > 1: case len(archives) > 1:
return "too many matches" // See if the second part is a SemVer
case len(archives) == 1: found := []string{}
archive := archives[0] for _, arc := range archives {
if _, err := os.Stat(archive); err == nil { // we need to trip the prefix dirs and the extension off.
c, err := loader.Load(archive) filename = strings.TrimSuffix(filepath.Base(arc), ".tgz")
if err != nil { maybeVersion := strings.TrimPrefix(filename, fmt.Sprintf("%s-", dep.Name))
return "corrupt"
if _, err := semver.StrictNewVersion(maybeVersion); err == nil {
// If the version parsed without an error, it is possibly a valid
// version.
found = append(found, arc)
} }
if c.Name() != dep.Name { }
return "misnamed"
if l := len(found); l == 1 {
// If we get here, we do the same thing as in len(archives) == 1.
if r := statArchiveForStatus(found[0], dep); r != "" {
return r
} }
if c.Metadata.Version != dep.Version { // Fall through and look for directories
constraint, err := semver.NewConstraint(dep.Version) } else if l > 1 {
if err != nil { return "too many matches"
return "invalid version" }
}
v, err := semver.NewVersion(c.Metadata.Version) // The sanest thing to do here is to fall through and see if we have any directory
if err != nil { // matches.
return "invalid version"
}
if !constraint.Check(v) { case len(archives) == 1:
return "wrong version" archive := archives[0]
} if r := statArchiveForStatus(archive, dep); r != "" {
} return r
return "ok"
} }
} }
// End unnecessary code. // End unnecessary code.
@ -137,6 +144,40 @@ func (d *Dependency) dependencyStatus(chartpath string, dep *chart.Dependency, p
return "unpacked" return "unpacked"
} }
// stat an archive and return a message if the stat is successful
//
// This is a refactor of the code originally in dependencyStatus. It is here to
// support legacy behavior, and should be removed in Helm 4.
func statArchiveForStatus(archive string, dep *chart.Dependency) string {
if _, err := os.Stat(archive); err == nil {
c, err := loader.Load(archive)
if err != nil {
return "corrupt"
}
if c.Name() != dep.Name {
return "misnamed"
}
if c.Metadata.Version != dep.Version {
constraint, err := semver.NewConstraint(dep.Version)
if err != nil {
return "invalid version"
}
v, err := semver.NewVersion(c.Metadata.Version)
if err != nil {
return "invalid version"
}
if !constraint.Check(v) {
return "wrong version"
}
}
return "ok"
}
return ""
}
// printDependencies prints all of the dependencies in the yaml file. // printDependencies prints all of the dependencies in the yaml file.
func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) { func (d *Dependency) printDependencies(chartpath string, out io.Writer, c *chart.Chart) {
table := uitable.New() table := uitable.New()

@ -18,9 +18,16 @@ package action
import ( import (
"bytes" "bytes"
"io/ioutil"
"os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert"
"helm.sh/helm/v3/internal/test" "helm.sh/helm/v3/internal/test"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chartutil"
) )
func TestList(t *testing.T) { func TestList(t *testing.T) {
@ -56,3 +63,99 @@ func TestList(t *testing.T) {
test.AssertGoldenBytes(t, buf.Bytes(), tcase.golden) test.AssertGoldenBytes(t, buf.Bytes(), tcase.golden)
} }
} }
// TestDependencyStatus_Dashes is a regression test to make sure that dashes in
// chart names do not cause resolution problems.
func TestDependencyStatus_Dashes(t *testing.T) {
// Make a temp dir
dir, err := ioutil.TempDir("", "helmtest-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
chartpath := filepath.Join(dir, "charts")
if err := os.MkdirAll(chartpath, 0700); err != nil {
t.Fatal(err)
}
// Add some fake charts
first := buildChart(withName("first-chart"))
_, err = chartutil.Save(first, chartpath)
if err != nil {
t.Fatal(err)
}
second := buildChart(withName("first-chart-second-chart"))
_, err = chartutil.Save(second, chartpath)
if err != nil {
t.Fatal(err)
}
dep := &chart.Dependency{
Name: "first-chart",
Version: "0.1.0",
}
// Now try to get the deps
stat := NewDependency().dependencyStatus(dir, dep, first)
if stat != "ok" {
t.Errorf("Unexpected status: %q", stat)
}
}
func TestStatArchiveForStatus(t *testing.T) {
// Make a temp dir
dir, err := ioutil.TempDir("", "helmtest-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
chartpath := filepath.Join(dir, "charts")
if err := os.MkdirAll(chartpath, 0700); err != nil {
t.Fatal(err)
}
// unsaved chart
lilith := buildChart(withName("lilith"))
// dep referring to chart
dep := &chart.Dependency{
Name: "lilith",
Version: "1.2.3",
}
is := assert.New(t)
lilithpath := filepath.Join(chartpath, "lilith-1.2.3.tgz")
is.Empty(statArchiveForStatus(lilithpath, dep))
// save the chart (version 0.1.0, because that is the default)
where, err := chartutil.Save(lilith, chartpath)
is.NoError(err)
// Should get "wrong version" because we asked for 1.2.3 and got 0.1.0
is.Equal("wrong version", statArchiveForStatus(where, dep))
// Break version on dep
dep = &chart.Dependency{
Name: "lilith",
Version: "1.2.3.4.5",
}
is.Equal("invalid version", statArchiveForStatus(where, dep))
// Break the name
dep = &chart.Dependency{
Name: "lilith2",
Version: "1.2.3",
}
is.Equal("misnamed", statArchiveForStatus(where, dep))
// Now create the right version
dep = &chart.Dependency{
Name: "lilith",
Version: "0.1.0",
}
is.Equal("ok", statArchiveForStatus(where, dep))
}

@ -19,6 +19,7 @@ package action
import ( import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
) )
@ -45,7 +46,7 @@ func (h *History) Run(name string) ([]*release.Release, error) {
return nil, err return nil, err
} }
if err := validateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name) return nil, errors.Errorf("release name is invalid: %s", name)
} }

@ -654,8 +654,8 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (
dl.Verify = downloader.VerifyAlways dl.Verify = downloader.VerifyAlways
} }
if c.RepoURL != "" { if c.RepoURL != "" {
chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version, chartURL, err := repo.FindChartInAuthAndTLSRepoURL(c.RepoURL, c.Username, c.Password, name, version,
c.CertFile, c.KeyFile, c.CaFile, getter.All(settings)) c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, getter.All(settings))
if err != nil { if err != nil {
return "", err return "", err
} }

@ -17,6 +17,7 @@ limitations under the License.
package action package action
import ( import (
"bufio"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
@ -39,6 +40,7 @@ type Package struct {
Sign bool Sign bool
Key string Key string
Keyring string Keyring string
PassphraseFile string
Version string Version string
AppVersion string AppVersion string
Destination string Destination string
@ -120,7 +122,15 @@ func (p *Package) Clearsign(filename string) error {
return err return err
} }
if err := signer.DecryptKey(promptUser); err != nil { passphraseFetcher := promptUser
if p.PassphraseFile != "" {
passphraseFetcher, err = passphraseFileFetcher(p.PassphraseFile, os.Stdin)
if err != nil {
return err
}
}
if err := signer.DecryptKey(passphraseFetcher); err != nil {
return err return err
} }
@ -141,3 +151,34 @@ func promptUser(name string) ([]byte, error) {
fmt.Println() fmt.Println()
return pw, err return pw, err
} }
func passphraseFileFetcher(passphraseFile string, stdin *os.File) (provenance.PassphraseFetcher, error) {
file, err := openPassphraseFile(passphraseFile, stdin)
if err != nil {
return nil, err
}
defer file.Close()
reader := bufio.NewReader(file)
passphrase, _, err := reader.ReadLine()
if err != nil {
return nil, err
}
return func(name string) ([]byte, error) {
return passphrase, nil
}, nil
}
func openPassphraseFile(passphraseFile string, stdin *os.File) (*os.File, error) {
if passphraseFile == "-" {
stat, err := stdin.Stat()
if err != nil {
return nil, err
}
if (stat.Mode() & os.ModeNamedPipe) == 0 {
return nil, errors.New("specified reading passphrase from stdin, without input on stdin")
}
return stdin, nil
}
return os.Open(passphraseFile)
}

@ -17,8 +17,12 @@ limitations under the License.
package action package action
import ( import (
"io/ioutil"
"os"
"path"
"testing" "testing"
"helm.sh/helm/v3/internal/test/ensure"
"helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chart"
) )
@ -42,3 +46,57 @@ func TestSetVersion(t *testing.T) {
t.Error("Expected bogus version to return an error.") t.Error("Expected bogus version to return an error.")
} }
} }
func TestPassphraseFileFetcher(t *testing.T) {
secret := "secret"
directory := ensure.TempFile(t, "passphrase-file", []byte(secret))
defer os.RemoveAll(directory)
fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
if err != nil {
t.Fatal("Unable to create passphraseFileFetcher", err)
}
passphrase, err := fetcher("key")
if err != nil {
t.Fatal("Unable to fetch passphrase")
}
if string(passphrase) != secret {
t.Errorf("Expected %s got %s", secret, string(passphrase))
}
}
func TestPassphraseFileFetcher_WithLineBreak(t *testing.T) {
secret := "secret"
directory := ensure.TempFile(t, "passphrase-file", []byte(secret+"\n\n."))
defer os.RemoveAll(directory)
fetcher, err := passphraseFileFetcher(path.Join(directory, "passphrase-file"), nil)
if err != nil {
t.Fatal("Unable to create passphraseFileFetcher", err)
}
passphrase, err := fetcher("key")
if err != nil {
t.Fatal("Unable to fetch passphrase")
}
if string(passphrase) != secret {
t.Errorf("Expected %s got %s", secret, string(passphrase))
}
}
func TestPassphraseFileFetcher_WithInvalidStdin(t *testing.T) {
directory := ensure.TempDir(t)
defer os.RemoveAll(directory)
stdin, err := ioutil.TempFile(directory, "non-existing")
if err != nil {
t.Fatal("Unable to create test file", err)
}
if _, err := passphraseFileFetcher("-", stdin); err == nil {
t.Error("Expected passphraseFileFetcher returning an error")
}
}

@ -89,7 +89,7 @@ func (p *Pull) Run(chartRef string) (string, error) {
} }
if p.RepoURL != "" { if p.RepoURL != "" {
chartURL, err := repo.FindChartInAuthRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, getter.All(p.Settings)) chartURL, err := repo.FindChartInAuthAndTLSRepoURL(p.RepoURL, p.Username, p.Password, chartRef, p.Version, p.CertFile, p.KeyFile, p.CaFile, p.InsecureSkipTLSverify, getter.All(p.Settings))
if err != nil { if err != nil {
return out.String(), err return out.String(), err
} }

@ -25,6 +25,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
) )
@ -51,7 +52,7 @@ func (r *ReleaseTesting) Run(name string) (*release.Release, error) {
return nil, err return nil, err
} }
if err := validateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("releaseTest: Release name is invalid: %s", name) return nil, errors.Errorf("releaseTest: Release name is invalid: %s", name)
} }

@ -24,6 +24,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
) )
@ -90,7 +91,7 @@ func (r *Rollback) Run(name string) error {
// prepareRollback finds the previous release and prepares a new release object with // prepareRollback finds the previous release and prepares a new release object with
// the previous release's configuration // the previous release's configuration
func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) { func (r *Rollback) prepareRollback(name string) (*release.Release, *release.Release, error) {
if err := validateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name) return nil, nil, errors.Errorf("prepareRollback: Release name is invalid: %s", name)
} }

@ -22,6 +22,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/releaseutil" "helm.sh/helm/v3/pkg/releaseutil"
helmtime "helm.sh/helm/v3/pkg/time" helmtime "helm.sh/helm/v3/pkg/time"
@ -62,7 +63,7 @@ func (u *Uninstall) Run(name string) (*release.UninstallReleaseResponse, error)
return &release.UninstallReleaseResponse{Release: r}, nil return &release.UninstallReleaseResponse{Release: r}, nil
} }
if err := validateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("uninstall: Release name is invalid: %s", name) return nil, errors.Errorf("uninstall: Release name is invalid: %s", name)
} }

@ -115,7 +115,7 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface
// the user doesn't have to specify both // the user doesn't have to specify both
u.Wait = u.Wait || u.Atomic u.Wait = u.Wait || u.Atomic
if err := validateReleaseName(name); err != nil { if err := chartutil.ValidateReleaseName(name); err != nil {
return nil, errors.Errorf("release name is invalid: %s", name) return nil, errors.Errorf("release name is invalid: %s", name)
} }
u.cfg.Log("preparing upgrade for %s", name) u.cfg.Log("preparing upgrade for %s", name)
@ -142,19 +142,6 @@ func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface
return res, nil return res, nil
} }
func validateReleaseName(releaseName string) error {
if releaseName == "" {
return errMissingRelease
}
// Check length first, since that is a less expensive operation.
if len(releaseName) > releaseNameMaxLen || !ValidName.MatchString(releaseName) {
return errInvalidName
}
return nil
}
// prepareUpgrade builds an upgraded release for an upgrade operation. // prepareUpgrade builds an upgraded release for an upgrade operation.
func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) { func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, *release.Release, error) {
if chart == nil { if chart == nil {

@ -17,6 +17,7 @@ package chart
import ( import (
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
) )
@ -26,6 +27,9 @@ const APIVersionV1 = "v1"
// APIVersionV2 is the API version number for version 2. // APIVersionV2 is the API version number for version 2.
const APIVersionV2 = "v2" const APIVersionV2 = "v2"
// aliasNameFormat defines the characters that are legal in an alias name.
var aliasNameFormat = regexp.MustCompile("^[a-zA-Z0-9_-]+$")
// Chart is a helm package that contains metadata, a default config, zero or more // Chart is a helm package that contains metadata, a default config, zero or more
// optionally parameterizable templates, and zero or more charts (dependencies). // optionally parameterizable templates, and zero or more charts (dependencies).
type Chart struct { type Chart struct {

@ -15,9 +15,16 @@ limitations under the License.
package chart package chart
import "fmt"
// ValidationError represents a data validation error. // ValidationError represents a data validation error.
type ValidationError string type ValidationError string
func (v ValidationError) Error() string { func (v ValidationError) Error() string {
return "validation: " + string(v) return "validation: " + string(v)
} }
// ValidationErrorf takes a message and formatting options and creates a ValidationError
func ValidationErrorf(msg string, args ...interface{}) ValidationError {
return ValidationError(fmt.Sprintf(msg, args...))
}

@ -81,6 +81,15 @@ func (md *Metadata) Validate() error {
if !isValidChartType(md.Type) { if !isValidChartType(md.Type) {
return ValidationError("chart.metadata.type must be application or library") return ValidationError("chart.metadata.type must be application or library")
} }
// Aliases need to be validated here to make sure that the alias name does
// not contain any illegal characters.
for _, dependency := range md.Dependencies {
if err := validateDependency(dependency); err != nil {
return err
}
}
// TODO validate valid semver here? // TODO validate valid semver here?
return nil return nil
} }
@ -92,3 +101,13 @@ func isValidChartType(in string) bool {
} }
return false return false
} }
// validateDependency checks for common problems with the dependency datastructure in
// the chart. This check must be done at load time before the dependency's charts are
// loaded.
func validateDependency(dep *Dependency) error {
if len(dep.Alias) > 0 && !aliasNameFormat.MatchString(dep.Alias) {
return ValidationErrorf("dependency %q has disallowed characters in the alias", dep.Name)
}
return nil
}

@ -48,12 +48,60 @@ func TestValidate(t *testing.T) {
&Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"}, &Metadata{Name: "test", APIVersion: "v2", Version: "1.0", Type: "application"},
nil, nil,
}, },
{
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
{Name: "dependency", Alias: "legal-alias"},
},
},
nil,
},
{
&Metadata{
Name: "test",
APIVersion: "v2",
Version: "1.0",
Type: "application",
Dependencies: []*Dependency{
{Name: "bad", Alias: "illegal alias"},
},
},
ValidationError("dependency \"bad\" has disallowed characters in the alias"),
},
} }
for _, tt := range tests { for _, tt := range tests {
result := tt.md.Validate() result := tt.md.Validate()
if result != tt.err { if result != tt.err {
t.Errorf("expected %s, got %s", tt.err, result) t.Errorf("expected '%s', got '%s'", tt.err, result)
}
}
}
func TestValidateDependency(t *testing.T) {
dep := &Dependency{
Name: "example",
}
for value, shouldFail := range map[string]bool{
"abcdefghijklmenopQRSTUVWXYZ-0123456780_": false,
"-okay": false,
"_okay": false,
"- bad": true,
" bad": true,
"bad\nvalue": true,
"bad ": true,
"bad$": true,
} {
dep.Alias = value
res := validateDependency(dep)
if res != nil && !shouldFail {
t.Errorf("Failed on case %q", dep.Alias)
} else if res == nil && shouldFail {
t.Errorf("Expected failure for %q", dep.Alias)
} }
} }
} }

@ -18,9 +18,11 @@ package chartutil
import ( import (
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"strings" "strings"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -30,6 +32,12 @@ import (
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
) )
// chartName is a regular expression for testing the supplied name of a chart.
// This regular expression is probably stricter than it needs to be. We can relax it
// somewhat. Newline characters, as well as $, quotes, +, parens, and % are known to be
// problematic.
var chartName = regexp.MustCompile("^[a-zA-Z0-9._-]+$")
const ( const (
// ChartfileName is the default Chart file name. // ChartfileName is the default Chart file name.
ChartfileName = "Chart.yaml" ChartfileName = "Chart.yaml"
@ -63,6 +71,10 @@ const (
TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml" TestConnectionName = TemplatesTestsDir + sep + "test-connection.yaml"
) )
// maxChartNameLength is lower than the limits we know of with certain file systems,
// and with certain Kubernetes fields.
const maxChartNameLength = 250
const sep = string(filepath.Separator) const sep = string(filepath.Separator)
const defaultChartfile = `apiVersion: v2 const defaultChartfile = `apiVersion: v2
@ -425,7 +437,9 @@ Common labels
{{- define "<CHARTNAME>.labels" -}} {{- define "<CHARTNAME>.labels" -}}
helm.sh/chart: {{ include "<CHARTNAME>.chart" . }} helm.sh/chart: {{ include "<CHARTNAME>.chart" . }}
{{ include "<CHARTNAME>.selectorLabels" . }} {{ include "<CHARTNAME>.selectorLabels" . }}
app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }} {{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }} {{- end }}
@ -466,6 +480,12 @@ spec:
restartPolicy: Never restartPolicy: Never
` `
// Stderr is an io.Writer to which error messages can be written
//
// In Helm 4, this will be replaced. It is needed in Helm 3 to preserve API backward
// compatibility.
var Stderr io.Writer = os.Stderr
// CreateFrom creates a new chart, but scaffolds it from the src chart. // CreateFrom creates a new chart, but scaffolds it from the src chart.
func CreateFrom(chartfile *chart.Metadata, dest, src string) error { func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
schart, err := loader.Load(src) schart, err := loader.Load(src)
@ -520,6 +540,12 @@ func CreateFrom(chartfile *chart.Metadata, dest, src string) error {
// error. In such a case, this will attempt to clean up by removing the // error. In such a case, this will attempt to clean up by removing the
// new chart directory. // new chart directory.
func Create(name, dir string) (string, error) { func Create(name, dir string) (string, error) {
// Sanity-check the name of a chart so user doesn't create one that causes problems.
if err := validateChartName(name); err != nil {
return "", err
}
path, err := filepath.Abs(dir) path, err := filepath.Abs(dir)
if err != nil { if err != nil {
return path, err return path, err
@ -599,8 +625,8 @@ func Create(name, dir string) (string, error) {
for _, file := range files { for _, file := range files {
if _, err := os.Stat(file.path); err == nil { if _, err := os.Stat(file.path); err == nil {
// File exists and is okay. Skip it. // There is no handle to a preferred output stream here.
continue fmt.Fprintf(Stderr, "WARNING: File %q already exists. Overwriting.\n", file.path)
} }
if err := writeFile(file.path, file.content); err != nil { if err := writeFile(file.path, file.content); err != nil {
return cdir, err return cdir, err
@ -625,3 +651,13 @@ func writeFile(name string, content []byte) error {
} }
return ioutil.WriteFile(name, content, 0644) return ioutil.WriteFile(name, content, 0644)
} }
func validateChartName(name string) error {
if name == "" || len(name) > maxChartNameLength {
return fmt.Errorf("chart name must be between 1 and %d characters", maxChartNameLength)
}
if !chartName.MatchString(name) {
return fmt.Errorf("chart name must match the regular expression %q", chartName.String())
}
return nil
}

@ -117,3 +117,69 @@ func TestCreateFrom(t *testing.T) {
} }
} }
} }
// TestCreate_Overwrite is a regression test for making sure that files are overwritten.
func TestCreate_Overwrite(t *testing.T) {
tdir, err := ioutil.TempDir("", "helm-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tdir)
var errlog bytes.Buffer
if _, err := Create("foo", tdir); err != nil {
t.Fatal(err)
}
dir := filepath.Join(tdir, "foo")
tplname := filepath.Join(dir, "templates/hpa.yaml")
writeFile(tplname, []byte("FOO"))
// Now re-run the create
Stderr = &errlog
if _, err := Create("foo", tdir); err != nil {
t.Fatal(err)
}
data, err := ioutil.ReadFile(tplname)
if err != nil {
t.Fatal(err)
}
if string(data) == "FOO" {
t.Fatal("File that should have been modified was not.")
}
if errlog.Len() == 0 {
t.Errorf("Expected warnings about overwriting files.")
}
}
func TestValidateChartName(t *testing.T) {
for name, shouldPass := range map[string]bool{
"": false,
"abcdefghijklmnopqrstuvwxyz-_.": true,
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": true,
"$hello": false,
"Hellô": false,
"he%%o": false,
"he\nllo": false,
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"abcdefghijklmnopqrstuvwxyz-_." +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ-_.": false,
} {
if err := validateChartName(name); (err != nil) == shouldPass {
t.Errorf("test for %q failed", name)
}
}
}

@ -0,0 +1,99 @@
/*
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 chartutil
import (
"regexp"
"github.com/pkg/errors"
)
// validName is a regular expression for resource names.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
var (
// errMissingName indicates that a release (name) was not provided.
errMissingName = errors.New("no name provided")
// errInvalidName indicates that an invalid release name was provided
errInvalidName = errors.New("invalid release name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 53")
// errInvalidKubernetesName indicates that the name does not meet the Kubernetes
// restrictions on metadata names.
errInvalidKubernetesName = errors.New("invalid metadata name, must match regex ^(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])+$ and the length must not longer than 253")
)
const (
// maxNameLen is the maximum length Helm allows for a release name
maxReleaseNameLen = 53
// maxMetadataNameLen is the maximum length Kubernetes allows for any name.
maxMetadataNameLen = 253
)
// ValidateReleaseName performs checks for an entry for a Helm release name
//
// For Helm to allow a name, it must be below a certain character count (53) and also match
// a reguar expression.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
func ValidateReleaseName(name string) error {
// This case is preserved for backwards compatibility
if name == "" {
return errMissingName
}
if len(name) > maxReleaseNameLen || !validName.MatchString(name) {
return errInvalidName
}
return nil
}
// ValidateMetadataName validates the name field of a Kubernetes metadata object.
//
// Empty strings, strings longer than 253 chars, or strings that don't match the regexp
// will fail.
//
// According to the Kubernetes help text, the regular expression it uses is:
//
// [a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*
//
// This follows the above regular expression (but requires a full string match, not partial).
//
// The Kubernetes documentation is here, though it is not entirely correct:
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
func ValidateMetadataName(name string) error {
if name == "" || len(name) > maxMetadataNameLen || !validName.MatchString(name) {
return errInvalidKubernetesName
}
return nil
}

@ -0,0 +1,91 @@
/*
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 chartutil
import "testing"
// TestValidateName is a regression test for ValidateName
//
// Kubernetes has strict naming conventions for resource names. This test represents
// those conventions.
//
// See https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
//
// NOTE: At the time of this writing, the docs above say that names cannot begin with
// digits. However, `kubectl`'s regular expression explicit allows this, and
// Kubernetes (at least as of 1.18) also accepts resources whose names begin with digits.
func TestValidateReleaseName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
if err := ValidateReleaseName(input); (err == nil) != expectPass {
st := "fail"
if expectPass {
st = "succeed"
}
t.Errorf("Expected %q to %s", input, st)
}
}
}
func TestValidateMetadataName(t *testing.T) {
names := map[string]bool{
"": false,
"foo": true,
"foo.bar1234baz.seventyone": true,
"FOO": false,
"123baz": true,
"foo.BAR.baz": false,
"one-two": true,
"-two": false,
"one_two": false,
"a..b": false,
"%^&#$%*@^*@&#^": false,
"example:com": false,
"example%%com": false,
"a1111111111111111111111111111111111111111111111111111111111z": true,
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z" +
"a1111111111111111111111111111111111111111111111111111111111z": false,
}
for input, expectPass := range names {
if err := ValidateMetadataName(input); (err == nil) != expectPass {
st := "fail"
if expectPass {
st = "succeed"
}
t.Errorf("Expected %q to %s", input, st)
}
}
}

@ -26,6 +26,7 @@ import (
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
@ -47,6 +48,10 @@ type EnvSettings struct {
KubeContext string KubeContext string
// Bearer KubeToken used for authentication // Bearer KubeToken used for authentication
KubeToken string KubeToken string
// Username to impersonate for the operation
KubeAsUser string
// Groups to impersonate for the operation, multiple groups parsed from a comma delimited list
KubeAsGroups []string
// Kubernetes API Server Endpoint for authentication // Kubernetes API Server Endpoint for authentication
KubeAPIServer string KubeAPIServer string
// Debug indicates whether or not Helm is running in Debug mode. // Debug indicates whether or not Helm is running in Debug mode.
@ -69,6 +74,8 @@ func New() *EnvSettings {
MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory), MaxHistory: envIntOr("HELM_MAX_HISTORY", defaultMaxHistory),
KubeContext: os.Getenv("HELM_KUBECONTEXT"), KubeContext: os.Getenv("HELM_KUBECONTEXT"),
KubeToken: os.Getenv("HELM_KUBETOKEN"), KubeToken: os.Getenv("HELM_KUBETOKEN"),
KubeAsUser: os.Getenv("HELM_KUBEASUSER"),
KubeAsGroups: envCSV("HELM_KUBEASGROUPS"),
KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"), KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"),
PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")),
RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")), RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")),
@ -79,11 +86,13 @@ func New() *EnvSettings {
// bind to kubernetes config flags // bind to kubernetes config flags
env.config = &genericclioptions.ConfigFlags{ env.config = &genericclioptions.ConfigFlags{
Namespace: &env.namespace, Namespace: &env.namespace,
Context: &env.KubeContext, Context: &env.KubeContext,
BearerToken: &env.KubeToken, BearerToken: &env.KubeToken,
APIServer: &env.KubeAPIServer, APIServer: &env.KubeAPIServer,
KubeConfig: &env.KubeConfig, KubeConfig: &env.KubeConfig,
Impersonate: &env.KubeAsUser,
ImpersonateGroup: &env.KubeAsGroups,
} }
return env return env
} }
@ -94,6 +103,8 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file") fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file")
fs.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "name of the kubeconfig context to use") fs.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "name of the kubeconfig context to use")
fs.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "bearer token used for authentication") fs.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "bearer token used for authentication")
fs.StringVar(&s.KubeAsUser, "kube-as-user", s.KubeAsUser, "Username to impersonate for the operation")
fs.StringArrayVar(&s.KubeAsGroups, "kube-as-group", s.KubeAsGroups, "Group to impersonate for the operation, this flag can be repeated to specify multiple groups.")
fs.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "the address and the port for the Kubernetes API server") fs.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "the address and the port for the Kubernetes API server")
fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output") fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output")
fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file") fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file")
@ -120,6 +131,14 @@ func envIntOr(name string, def int) int {
return ret return ret
} }
func envCSV(name string) (ls []string) {
trimmed := strings.Trim(os.Getenv(name), ", ")
if trimmed != "" {
ls = strings.Split(trimmed, ",")
}
return
}
func (s *EnvSettings) EnvVars() map[string]string { func (s *EnvSettings) EnvVars() map[string]string {
envvars := map[string]string{ envvars := map[string]string{
"HELM_BIN": os.Args[0], "HELM_BIN": os.Args[0],
@ -137,6 +156,8 @@ func (s *EnvSettings) EnvVars() map[string]string {
// broken, these are populated from helm flags and not kubeconfig. // broken, these are populated from helm flags and not kubeconfig.
"HELM_KUBECONTEXT": s.KubeContext, "HELM_KUBECONTEXT": s.KubeContext,
"HELM_KUBETOKEN": s.KubeToken, "HELM_KUBETOKEN": s.KubeToken,
"HELM_KUBEASUSER": s.KubeAsUser,
"HELM_KUBEASGROUPS": strings.Join(s.KubeAsGroups, ","),
"HELM_KUBEAPISERVER": s.KubeAPIServer, "HELM_KUBEAPISERVER": s.KubeAPIServer,
} }
if s.KubeConfig != "" { if s.KubeConfig != "" {

@ -18,6 +18,7 @@ package cli
import ( import (
"os" "os"
"reflect"
"strings" "strings"
"testing" "testing"
@ -36,6 +37,8 @@ func TestEnvSettings(t *testing.T) {
ns, kcontext string ns, kcontext string
debug bool debug bool
maxhistory int maxhistory int
kAsUser string
kAsGroups []string
}{ }{
{ {
name: "defaults", name: "defaults",
@ -44,25 +47,31 @@ func TestEnvSettings(t *testing.T) {
}, },
{ {
name: "with flags set", name: "with flags set",
args: "--debug --namespace=myns", args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters",
ns: "myns", ns: "myns",
debug: true, debug: true,
maxhistory: defaultMaxHistory, maxhistory: defaultMaxHistory,
kAsUser: "poro",
kAsGroups: []string{"admins", "teatime", "snackeaters"},
}, },
{ {
name: "with envvars set", name: "with envvars set",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_MAX_HISTORY": "5"}, envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5"},
ns: "yourns", ns: "yourns",
maxhistory: 5, maxhistory: 5,
debug: true, debug: true,
kAsUser: "pikachu",
kAsGroups: []string{"operators", "snackeaters", "partyanimals"},
}, },
{ {
name: "with flags and envvars set", name: "with flags and envvars set",
args: "--debug --namespace=myns", args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters",
envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns"}, envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5"},
ns: "myns", ns: "myns",
debug: true, debug: true,
maxhistory: defaultMaxHistory, maxhistory: 5,
kAsUser: "poro",
kAsGroups: []string{"admins", "teatime", "snackeaters"},
}, },
} }
@ -92,6 +101,12 @@ func TestEnvSettings(t *testing.T) {
if settings.MaxHistory != tt.maxhistory { if settings.MaxHistory != tt.maxhistory {
t.Errorf("expected maxHistory %d, got %d", tt.maxhistory, settings.MaxHistory) t.Errorf("expected maxHistory %d, got %d", tt.maxhistory, settings.MaxHistory)
} }
if tt.kAsUser != settings.KubeAsUser {
t.Errorf("expected kAsUser %q, got %q", tt.kAsUser, settings.KubeAsUser)
}
if !reflect.DeepEqual(tt.kAsGroups, settings.KubeAsGroups) {
t.Errorf("expected kAsGroups %+v, got %+v", len(tt.kAsGroups), len(settings.KubeAsGroups))
}
}) })
} }
} }

@ -71,7 +71,7 @@ func TestResolveChartRef(t *testing.T) {
if tt.fail { if tt.fail {
continue continue
} }
t.Errorf("%s: failed with error %s", tt.name, err) t.Errorf("%s: failed with error %q", tt.name, err)
continue continue
} }
if got := u.String(); got != tt.expect { if got := u.String(); got != tt.expect {
@ -172,7 +172,7 @@ func TestIsTar(t *testing.T) {
func TestDownloadTo(t *testing.T) { func TestDownloadTo(t *testing.T) {
// Set up a fake repo with basic auth enabled // Set up a fake repo with basic auth enabled
srv, err := repotest.NewTempServer("testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
srv.Stop() srv.Stop()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -229,7 +229,7 @@ func TestDownloadTo(t *testing.T) {
func TestDownloadTo_TLS(t *testing.T) { func TestDownloadTo_TLS(t *testing.T) {
// Set up mock server w/ tls enabled // Set up mock server w/ tls enabled
srv, err := repotest.NewTempServer("testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
srv.Stop() srv.Stop()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -285,7 +285,7 @@ func TestDownloadTo_VerifyLater(t *testing.T) {
dest := ensure.TempDir(t) dest := ensure.TempDir(t)
// Set up a fake repo // Set up a fake repo
srv, err := repotest.NewTempServer("testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -183,7 +183,7 @@ func TestGetRepoNames(t *testing.T) {
func TestUpdateBeforeBuild(t *testing.T) { func TestUpdateBeforeBuild(t *testing.T) {
// Set up a fake repo // Set up a fake repo
srv, err := repotest.NewTempServer("testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -257,7 +257,7 @@ func TestUpdateBeforeBuild(t *testing.T) {
// If each of these main fields (name, version, repository) is not supplied by dep param, default value will be used. // If each of these main fields (name, version, repository) is not supplied by dep param, default value will be used.
func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Dependency) { func checkBuildWithOptionalFields(t *testing.T, chartName string, dep chart.Dependency) {
// Set up a fake repo // Set up a fake repo
srv, err := repotest.NewTempServer("testdata/*.tgz*") srv, err := repotest.NewTempServerWithCleanup(t, "testdata/*.tgz*")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -40,7 +40,7 @@ type Engine struct {
Strict bool Strict bool
// In LintMode, some 'required' template values may be missing, so don't fail // In LintMode, some 'required' template values may be missing, so don't fail
LintMode bool LintMode bool
// the rest config to connect to te kubernetes api // the rest config to connect to the kubernetes api
config *rest.Config config *rest.Config
} }

@ -91,9 +91,12 @@ func newResponse(code int, obj runtime.Object) (*http.Response, error) {
return &http.Response{StatusCode: code, Header: header, Body: body}, nil return &http.Response{StatusCode: code, Header: header, Body: body}, nil
} }
func newTestClient() *Client { func newTestClient(t *testing.T) *Client {
testFactory := cmdtesting.NewTestFactory()
t.Cleanup(testFactory.Cleanup)
return &Client{ return &Client{
Factory: cmdtesting.NewTestFactory().WithNamespace("default"), Factory: testFactory.WithNamespace("default"),
Log: nopLogger, Log: nopLogger,
} }
} }
@ -107,7 +110,7 @@ func TestUpdate(t *testing.T) {
var actions []string var actions []string
c := newTestClient() c := newTestClient(t)
c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{ c.Factory.(*cmdtesting.TestFactory).UnstructuredClient = &fake.RESTClient{
NegotiatedSerializer: unstructuredSerializer, NegotiatedSerializer: unstructuredSerializer,
Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { Client: fake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) {
@ -232,7 +235,7 @@ func TestBuild(t *testing.T) {
}, },
} }
c := newTestClient() c := newTestClient(t)
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Test for an invalid manifest // Test for an invalid manifest
@ -279,7 +282,7 @@ func TestPerform(t *testing.T) {
return nil return nil
} }
c := newTestClient() c := newTestClient(t)
infos, err := c.Build(tt.reader, false) infos, err := c.Build(tt.reader, false)
if err != nil && err.Error() != tt.errMessage { if err != nil && err.Error() != tt.errMessage {
t.Errorf("Error while building manifests: %v", err) t.Errorf("Error while building manifests: %v", err)

@ -40,14 +40,6 @@ var (
releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`) releaseTimeSearch = regexp.MustCompile(`\.Release\.Time`)
) )
// validName is a regular expression for names.
//
// This is different than action.ValidName. It conforms to the regular expression
// `kubectl` says it uses, plus it disallows empty names.
//
// For details, see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
var validName = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`)
// Templates lints the templates in the Linter. // 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, strict bool) {
fpath := "templates/" fpath := "templates/"
@ -199,10 +191,10 @@ func validateMetadataName(obj *K8sYamlStruct) error {
} }
// This will return an error if the characters do not abide by the standard OR if the // This will return an error if the characters do not abide by the standard OR if the
// name is left empty. // name is left empty.
if validName.MatchString(obj.Metadata.Name) { if err := chartutil.ValidateMetadataName(obj.Metadata.Name); err != nil {
return nil return errors.Wrapf(err, "object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name)
} }
return fmt.Errorf("object name does not conform to Kubernetes naming requirements: %q", obj.Metadata.Name) return nil
} }
func validateNoCRDHooks(manifest []byte) error { func validateNoCRDHooks(manifest []byte) error {

@ -59,6 +59,18 @@ var Extractors = map[string]Extractor{
".tgz": &TarGzExtractor{}, ".tgz": &TarGzExtractor{},
} }
// Convert a media type to an extractor extension.
//
// This should be refactored in Helm 4, combined with the extension-based mechanism.
func mediaTypeToExtension(mt string) (string, bool) {
switch strings.ToLower(mt) {
case "application/gzip", "application/x-gzip", "application/x-tgz", "application/x-gtar":
return ".tgz", true
default:
return "", false
}
}
// NewExtractor creates a new extractor matching the source file name // NewExtractor creates a new extractor matching the source file name
func NewExtractor(source string) (Extractor, error) { func NewExtractor(source string) (Extractor, error) {
for suffix, extractor := range Extractors { for suffix, extractor := range Extractors {

@ -20,9 +20,13 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"encoding/base64" "encoding/base64"
"fmt"
"io/ioutil" "io/ioutil"
"net/http"
"net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"testing" "testing"
@ -63,9 +67,24 @@ func TestStripName(t *testing.T) {
} }
} }
func mockArchiveServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasSuffix(r.URL.Path, ".tar.gz") {
w.Header().Add("Content-Type", "text/html")
fmt.Fprintln(w, "broken")
return
}
w.Header().Add("Content-Type", "application/gzip")
fmt.Fprintln(w, "test")
}))
}
func TestHTTPInstaller(t *testing.T) { func TestHTTPInstaller(t *testing.T) {
defer ensure.HelmHome(t)() defer ensure.HelmHome(t)()
source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz"
srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
@ -111,7 +130,9 @@ func TestHTTPInstaller(t *testing.T) {
func TestHTTPInstallerNonExistentVersion(t *testing.T) { func TestHTTPInstallerNonExistentVersion(t *testing.T) {
defer ensure.HelmHome(t)() defer ensure.HelmHome(t)()
source := "https://repo.localdomain/plugins/fake-plugin-0.0.2.tar.gz" srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err) t.Fatalf("Could not create %s: %s", helmpath.DataPath("plugins"), err)
@ -141,7 +162,9 @@ func TestHTTPInstallerNonExistentVersion(t *testing.T) {
} }
func TestHTTPInstallerUpdate(t *testing.T) { func TestHTTPInstallerUpdate(t *testing.T) {
source := "https://repo.localdomain/plugins/fake-plugin-0.0.1.tar.gz" srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
defer ensure.HelmHome(t)() defer ensure.HelmHome(t)()
if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil { if err := os.MkdirAll(helmpath.DataPath("plugins"), 0755); err != nil {
@ -307,3 +330,26 @@ func TestCleanJoin(t *testing.T) {
} }
} }
func TestMediaTypeToExtension(t *testing.T) {
for mt, shouldPass := range map[string]bool{
"": false,
"application/gzip": true,
"application/x-gzip": true,
"application/x-tgz": true,
"application/x-gtar": true,
"application/json": false,
} {
ext, ok := mediaTypeToExtension(mt)
if ok != shouldPass {
t.Errorf("Media type %q failed test", mt)
}
if shouldPass && ext == "" {
t.Errorf("Expected an extension but got empty string")
}
if !shouldPass && len(ext) != 0 {
t.Error("Expected extension to be empty for unrecognized type")
}
}
}

@ -18,6 +18,7 @@ package installer
import ( import (
"fmt" "fmt"
"log" "log"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -89,10 +90,29 @@ func isLocalReference(source string) bool {
} }
// isRemoteHTTPArchive checks if the source is a http/https url and is an archive // isRemoteHTTPArchive checks if the source is a http/https url and is an archive
//
// It works by checking whether the source looks like a URL and, if it does, running a
// HEAD operation to see if the remote resource is a file that we understand.
func isRemoteHTTPArchive(source string) bool { func isRemoteHTTPArchive(source string) bool {
if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") { if strings.HasPrefix(source, "http://") || strings.HasPrefix(source, "https://") {
res, err := http.Head(source)
if err != nil {
// If we get an error at the network layer, we can't install it. So
// we return false.
return false
}
// Next, we look for the content type or content disposition headers to see
// if they have matching extractors.
contentType := res.Header.Get("content-type")
foundSuffix, ok := mediaTypeToExtension(contentType)
if !ok {
// Media type not recognized
return false
}
for suffix := range Extractors { for suffix := range Extractors {
if strings.HasSuffix(source, suffix) { if strings.HasSuffix(foundSuffix, suffix) {
return true return true
} }
} }

@ -0,0 +1,40 @@
/*
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 installer
import "testing"
func TestIsRemoteHTTPArchive(t *testing.T) {
srv := mockArchiveServer()
defer srv.Close()
source := srv.URL + "/plugins/fake-plugin-0.0.1.tar.gz"
if isRemoteHTTPArchive("/not/a/URL") {
t.Errorf("Expected non-URL to return false")
}
if isRemoteHTTPArchive("https://127.0.0.1:123/fake/plugin-1.2.3.tgz") {
t.Errorf("Bad URL should not have succeeded.")
}
if !isRemoteHTTPArchive(source) {
t.Errorf("Expected %q to be a valid archive URL", source)
}
if isRemoteHTTPArchive(source + "-not-an-extension") {
t.Error("Expected media type match to fail")
}
}

@ -37,7 +37,7 @@ func TestLocalInstaller(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
source := "../testdata/plugdir/echo" source := "../testdata/plugdir/good/echo"
i, err := NewForSource(source, "") i, err := NewForSource(source, "")
if err != nil { if err != nil {
t.Fatalf("unexpected error: %s", err) t.Fatalf("unexpected error: %s", err)

@ -56,7 +56,7 @@ func TestVCSInstaller(t *testing.T) {
} }
source := "https://github.com/adamreese/helm-env" source := "https://github.com/adamreese/helm-env"
testRepoPath, _ := filepath.Abs("../testdata/plugdir/echo") testRepoPath, _ := filepath.Abs("../testdata/plugdir/good/echo")
repo := &testRepo{ repo := &testRepo{
local: testRepoPath, local: testRepoPath,
tags: []string{"0.1.0", "0.1.1"}, tags: []string{"0.1.0", "0.1.1"},

@ -20,9 +20,11 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"strings" "strings"
"github.com/pkg/errors"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
"helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli"
@ -94,6 +96,12 @@ type Metadata struct {
// Downloaders field is used if the plugin supply downloader mechanism // Downloaders field is used if the plugin supply downloader mechanism
// for special protocols. // for special protocols.
Downloaders []Downloaders `json:"downloaders"` Downloaders []Downloaders `json:"downloaders"`
// UseTunnelDeprecated indicates that this command needs a tunnel.
// Setting this will cause a number of side effects, such as the
// automatic setting of HELM_HOST.
// DEPRECATED and unused, but retained for backwards compatibility with Helm 2 plugins. Remove in Helm 4
UseTunnelDeprecated bool `json:"useTunnel,omitempty"`
} }
// Plugin represents a plugin. // Plugin represents a plugin.
@ -157,18 +165,51 @@ func (p *Plugin) PrepareCommand(extraArgs []string) (string, []string, error) {
return main, baseArgs, nil return main, baseArgs, nil
} }
// validPluginName is a regular expression that validates plugin names.
//
// Plugin names can only contain the ASCII characters a-z, A-Z, 0-9, _ and -.
var validPluginName = regexp.MustCompile("^[A-Za-z0-9_-]+$")
// validatePluginData validates a plugin's YAML data.
func validatePluginData(plug *Plugin, filepath string) error {
if !validPluginName.MatchString(plug.Metadata.Name) {
return fmt.Errorf("invalid plugin name at %q", filepath)
}
// We could also validate SemVer, executable, and other fields should we so choose.
return nil
}
func detectDuplicates(plugs []*Plugin) error {
names := map[string]string{}
for _, plug := range plugs {
if oldpath, ok := names[plug.Metadata.Name]; ok {
return fmt.Errorf(
"two plugins claim the name %q at %q and %q",
plug.Metadata.Name,
oldpath,
plug.Dir,
)
}
names[plug.Metadata.Name] = plug.Dir
}
return nil
}
// LoadDir loads a plugin from the given directory. // LoadDir loads a plugin from the given directory.
func LoadDir(dirname string) (*Plugin, error) { func LoadDir(dirname string) (*Plugin, error) {
data, err := ioutil.ReadFile(filepath.Join(dirname, PluginFileName)) pluginfile := filepath.Join(dirname, PluginFileName)
data, err := ioutil.ReadFile(pluginfile)
if err != nil { if err != nil {
return nil, err return nil, errors.Wrapf(err, "failed to read plugin at %q", pluginfile)
} }
plug := &Plugin{Dir: dirname} plug := &Plugin{Dir: dirname}
if err := yaml.Unmarshal(data, &plug.Metadata); err != nil { if err := yaml.UnmarshalStrict(data, &plug.Metadata); err != nil {
return nil, err return nil, errors.Wrapf(err, "failed to load plugin at %q", pluginfile)
} }
return plug, nil return plug, validatePluginData(plug, pluginfile)
} }
// LoadAll loads all plugins found beneath the base directory. // LoadAll loads all plugins found beneath the base directory.
@ -180,7 +221,7 @@ func LoadAll(basedir string) ([]*Plugin, error) {
scanpath := filepath.Join(basedir, "*", PluginFileName) scanpath := filepath.Join(basedir, "*", PluginFileName)
matches, err := filepath.Glob(scanpath) matches, err := filepath.Glob(scanpath)
if err != nil { if err != nil {
return plugins, err return plugins, errors.Wrapf(err, "failed to find plugins in %q", scanpath)
} }
if matches == nil { if matches == nil {
@ -195,7 +236,7 @@ func LoadAll(basedir string) ([]*Plugin, error) {
} }
plugins = append(plugins, p) plugins = append(plugins, p)
} }
return plugins, nil return plugins, detectDuplicates(plugins)
} }
// FindPlugins returns a list of YAML files that describe plugins. // FindPlugins returns a list of YAML files that describe plugins.

@ -16,6 +16,7 @@ limitations under the License.
package plugin // import "helm.sh/helm/v3/pkg/plugin" package plugin // import "helm.sh/helm/v3/pkg/plugin"
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -177,7 +178,7 @@ func TestNoMatchPrepareCommand(t *testing.T) {
} }
func TestLoadDir(t *testing.T) { func TestLoadDir(t *testing.T) {
dirname := "testdata/plugdir/hello" dirname := "testdata/plugdir/good/hello"
plug, err := LoadDir(dirname) plug, err := LoadDir(dirname)
if err != nil { if err != nil {
t.Fatalf("error loading Hello plugin: %s", err) t.Fatalf("error loading Hello plugin: %s", err)
@ -204,8 +205,15 @@ func TestLoadDir(t *testing.T) {
} }
} }
func TestLoadDirDuplicateEntries(t *testing.T) {
dirname := "testdata/plugdir/bad/duplicate-entries"
if _, err := LoadDir(dirname); err == nil {
t.Errorf("successfully loaded plugin with duplicate entries when it should've failed")
}
}
func TestDownloader(t *testing.T) { func TestDownloader(t *testing.T) {
dirname := "testdata/plugdir/downloader" dirname := "testdata/plugdir/good/downloader"
plug, err := LoadDir(dirname) plug, err := LoadDir(dirname)
if err != nil { if err != nil {
t.Fatalf("error loading Hello plugin: %s", err) t.Fatalf("error loading Hello plugin: %s", err)
@ -243,7 +251,7 @@ func TestLoadAll(t *testing.T) {
t.Fatalf("expected empty dir to have 0 plugins") t.Fatalf("expected empty dir to have 0 plugins")
} }
basedir := "testdata/plugdir" basedir := "testdata/plugdir/good"
plugs, err := LoadAll(basedir) plugs, err := LoadAll(basedir)
if err != nil { if err != nil {
t.Fatalf("Could not load %q: %s", basedir, err) t.Fatalf("Could not load %q: %s", basedir, err)
@ -287,7 +295,7 @@ func TestFindPlugins(t *testing.T) {
}, },
{ {
name: "normal", name: "normal",
plugdirs: "./testdata/plugdir", plugdirs: "./testdata/plugdir/good",
expected: 3, expected: 3,
}, },
} }
@ -320,3 +328,51 @@ func TestSetupEnv(t *testing.T) {
} }
} }
} }
func TestValidatePluginData(t *testing.T) {
for i, item := range []struct {
pass bool
plug *Plugin
}{
{true, mockPlugin("abcdefghijklmnopqrstuvwxyz0123456789_-ABC")},
{true, mockPlugin("foo-bar-FOO-BAR_1234")},
{false, mockPlugin("foo -bar")},
{false, mockPlugin("$foo -bar")}, // Test leading chars
{false, mockPlugin("foo -bar ")}, // Test trailing chars
{false, mockPlugin("foo\nbar")}, // Test newline
} {
err := validatePluginData(item.plug, fmt.Sprintf("test-%d", i))
if item.pass && err != nil {
t.Errorf("failed to validate case %d: %s", i, err)
} else if !item.pass && err == nil {
t.Errorf("expected case %d to fail", i)
}
}
}
func TestDetectDuplicates(t *testing.T) {
plugs := []*Plugin{
mockPlugin("foo"),
mockPlugin("bar"),
}
if err := detectDuplicates(plugs); err != nil {
t.Error("no duplicates in the first set")
}
plugs = append(plugs, mockPlugin("foo"))
if err := detectDuplicates(plugs); err == nil {
t.Error("duplicates in the second set")
}
}
func mockPlugin(name string) *Plugin {
return &Plugin{
Metadata: &Metadata{
Name: name,
Version: "v0.1.2",
Usage: "Mock plugin",
Description: "Mock plugin for testing",
Command: "echo mock plugin",
},
Dir: "no-such-dir",
}
}

@ -0,0 +1,11 @@
name: "duplicate-entries"
version: "0.1.0"
usage: "usage"
description: |-
description
command: "echo hello"
ignoreFlags: true
hooks:
install: "echo installing..."
hooks:
install: "echo installing something different"

@ -5,6 +5,5 @@ description: |-
description description
command: "$HELM_PLUGIN_SELF/hello.sh" command: "$HELM_PLUGIN_SELF/hello.sh"
ignoreFlags: true ignoreFlags: true
install: "echo installing..."
hooks: hooks:
install: "echo installing..." install: "echo installing..."

@ -205,6 +205,14 @@ func FindChartInRepoURL(repoURL, chartName, chartVersion, certFile, keyFile, caF
// without adding repo to repositories, like FindChartInRepoURL, // without adding repo to repositories, like FindChartInRepoURL,
// but it also receives credentials for the chart repository. // but it also receives credentials for the chart repository.
func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) { func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, getters getter.Providers) (string, error) {
return FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile, false, getters)
}
// FindChartInAuthRepoURL finds chart in chart repository pointed by repoURL
// without adding repo to repositories, like FindChartInRepoURL,
// but it also receives credentials and TLS verify flag for the chart repository.
// TODO Helm 4, FindChartInAuthAndTLSRepoURL should be integrated into FindChartInAuthRepoURL.
func FindChartInAuthAndTLSRepoURL(repoURL, username, password, chartName, chartVersion, certFile, keyFile, caFile string, insecureSkipTLSverify bool, getters getter.Providers) (string, error) {
// Download and write the index file to a temporary location // Download and write the index file to a temporary location
buf := make([]byte, 20) buf := make([]byte, 20)
@ -212,13 +220,14 @@ func FindChartInAuthRepoURL(repoURL, username, password, chartName, chartVersion
name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-") name := strings.ReplaceAll(base64.StdEncoding.EncodeToString(buf), "/", "-")
c := Entry{ c := Entry{
URL: repoURL, URL: repoURL,
Username: username, Username: username,
Password: password, Password: password,
CertFile: certFile, CertFile: certFile,
KeyFile: keyFile, KeyFile: keyFile,
CAFile: caFile, CAFile: caFile,
Name: name, Name: name,
InsecureSkipTLSverify: insecureSkipTLSverify,
} }
r, err := NewChartRepository(&c, getters) r, err := NewChartRepository(&c, getters)
if err != nil { if err != nil {

@ -276,6 +276,44 @@ func startLocalServerForTests(handler http.Handler) (*httptest.Server, error) {
return httptest.NewServer(handler), nil return httptest.NewServer(handler), nil
} }
// startLocalTLSServerForTests Start the local helm server with TLS
func startLocalTLSServerForTests(handler http.Handler) (*httptest.Server, error) {
if handler == nil {
fileBytes, err := ioutil.ReadFile("testdata/local-index.yaml")
if err != nil {
return nil, err
}
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write(fileBytes)
})
}
return httptest.NewTLSServer(handler), nil
}
func TestFindChartInAuthAndTLSRepoURL(t *testing.T) {
srv, err := startLocalTLSServerForTests(nil)
if err != nil {
t.Fatal(err)
}
defer srv.Close()
chartURL, err := FindChartInAuthAndTLSRepoURL(srv.URL, "", "", "nginx", "", "", "", "", true, getter.All(&cli.EnvSettings{}))
if err != nil {
t.Fatalf("%v", err)
}
if chartURL != "https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz" {
t.Errorf("%s is not the valid URL", chartURL)
}
// If the insecureSkipTLsverify is false, it will return an error that contains "x509: certificate signed by unknown authority".
_, err = FindChartInAuthAndTLSRepoURL(srv.URL, "", "", "nginx", "0.1.0", "", "", "", false, getter.All(&cli.EnvSettings{}))
if !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") {
t.Errorf("Expected TLS error for function FindChartInAuthAndTLSRepoURL not found, but got a different error (%v)", err)
}
}
func TestFindChartInRepoURL(t *testing.T) { func TestFindChartInRepoURL(t *testing.T) {
srv, err := startLocalServerForTests(nil) srv, err := startLocalServerForTests(nil)
if err != nil { if err != nil {

@ -77,6 +77,8 @@ func (c ChartVersions) Less(a, b int) bool {
// IndexFile represents the index file in a chart repository // IndexFile represents the index file in a chart repository
type IndexFile struct { type IndexFile struct {
// This is used ONLY for validation against chartmuseum's index files and is discarded after validation.
ServerInfo map[string]interface{} `json:"serverInfo,omitempty"`
APIVersion string `json:"apiVersion"` APIVersion string `json:"apiVersion"`
Generated time.Time `json:"generated"` Generated time.Time `json:"generated"`
Entries map[string]ChartVersions `json:"entries"` Entries map[string]ChartVersions `json:"entries"`
@ -228,6 +230,23 @@ type ChartVersion struct {
Created time.Time `json:"created,omitempty"` Created time.Time `json:"created,omitempty"`
Removed bool `json:"removed,omitempty"` Removed bool `json:"removed,omitempty"`
Digest string `json:"digest,omitempty"` Digest string `json:"digest,omitempty"`
// ChecksumDeprecated is deprecated in Helm 3, and therefore ignored. Helm 3 replaced
// this with Digest. However, with a strict YAML parser enabled, a field must be
// present on the struct for backwards compatibility.
ChecksumDeprecated string `json:"checksum,omitempty"`
// EngineDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
// YAML parser enabled, this field must be present.
EngineDeprecated string `json:"engine,omitempty"`
// TillerVersionDeprecated is deprecated in Helm 3, and therefore ignored. However, with a strict
// YAML parser enabled, this field must be present.
TillerVersionDeprecated string `json:"tillerVersion,omitempty"`
// URLDeprecated is deprectaed in Helm 3, superseded by URLs. It is ignored. However,
// with a strict YAML parser enabled, this must be present on the struct.
URLDeprecated string `json:"url,omitempty"`
} }
// IndexDirectory reads a (flat) directory and generates an index. // IndexDirectory reads a (flat) directory and generates an index.
@ -281,7 +300,7 @@ func IndexDirectory(dir, baseURL string) (*IndexFile, error) {
// This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails. // This will fail if API Version is not set (ErrNoAPIVersion) or if the unmarshal fails.
func loadIndex(data []byte) (*IndexFile, error) { func loadIndex(data []byte) (*IndexFile, error) {
i := &IndexFile{} i := &IndexFile{}
if err := yaml.Unmarshal(data, i); err != nil { if err := yaml.UnmarshalStrict(data, i); err != nil {
return i, err return i, err
} }
i.SortEntries() i.SortEntries()

@ -35,9 +35,31 @@ import (
) )
const ( const (
testfile = "testdata/local-index.yaml" testfile = "testdata/local-index.yaml"
unorderedTestfile = "testdata/local-index-unordered.yaml" chartmuseumtestfile = "testdata/chartmuseum-index.yaml"
testRepo = "test-repo" unorderedTestfile = "testdata/local-index-unordered.yaml"
testRepo = "test-repo"
indexWithDuplicates = `
apiVersion: v1
entries:
nginx:
- urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz
name: nginx
description: string
version: 0.2.0
home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
nginx:
- urls:
- https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: alpine
description: string
version: 1.0.0
home: https://github.com/something
digest: "sha256:1234567890abcdef"
`
) )
func TestIndexFile(t *testing.T) { func TestIndexFile(t *testing.T) {
@ -84,15 +106,43 @@ func TestIndexFile(t *testing.T) {
} }
func TestLoadIndex(t *testing.T) { func TestLoadIndex(t *testing.T) {
b, err := ioutil.ReadFile(testfile)
if err != nil { tests := []struct {
t.Fatal(err) Name string
Filename string
}{
{
Name: "regular index file",
Filename: testfile,
},
{
Name: "chartmuseum index file",
Filename: chartmuseumtestfile,
},
} }
i, err := loadIndex(b)
if err != nil { for _, tc := range tests {
t.Fatal(err) tc := tc
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
b, err := ioutil.ReadFile(tc.Filename)
if err != nil {
t.Fatal(err)
}
i, err := loadIndex(b)
if err != nil {
t.Fatal(err)
}
verifyLocalIndex(t, i)
})
}
}
// TestLoadIndex_Duplicates is a regression to make sure that we don't non-deterministically allow duplicate packages.
func TestLoadIndex_Duplicates(t *testing.T) {
if _, err := loadIndex([]byte(indexWithDuplicates)); err == nil {
t.Errorf("Expected an error when duplicate entries are present")
} }
verifyLocalIndex(t, i)
} }
func TestLoadIndexFile(t *testing.T) { func TestLoadIndexFile(t *testing.T) {

@ -21,6 +21,7 @@ import (
"net/http/httptest" "net/http/httptest"
"os" "os"
"path/filepath" "path/filepath"
"testing"
"helm.sh/helm/v3/internal/tlsutil" "helm.sh/helm/v3/internal/tlsutil"
@ -29,6 +30,19 @@ import (
"helm.sh/helm/v3/pkg/repo" "helm.sh/helm/v3/pkg/repo"
) )
// NewTempServerWithCleanup creates a server inside of a temp dir.
//
// If the passed in string is not "", it will be treated as a shell glob, and files
// will be copied from that path to the server's docroot.
//
// The caller is responsible for stopping the server.
// The temp dir will be removed by testing package automatically when test finished.
func NewTempServerWithCleanup(t *testing.T, glob string) (*Server, error) {
srv, err := NewTempServer(glob)
t.Cleanup(func() { os.RemoveAll(srv.docroot) })
return srv, err
}
// NewTempServer creates a server inside of a temp dir. // NewTempServer creates a server inside of a temp dir.
// //
// If the passed in string is not "", it will be treated as a shell glob, and files // If the passed in string is not "", it will be treated as a shell glob, and files
@ -36,6 +50,8 @@ import (
// //
// The caller is responsible for destroying the temp directory as well as stopping // The caller is responsible for destroying the temp directory as well as stopping
// the server. // the server.
//
// Deprecated: use NewTempServerWithCleanup
func NewTempServer(glob string) (*Server, error) { func NewTempServer(glob string) (*Server, error) {
tdir, err := ioutil.TempDir("", "helm-repotest-") tdir, err := ioutil.TempDir("", "helm-repotest-")
if err != nil { if err != nil {

@ -99,7 +99,7 @@ func TestServer(t *testing.T) {
func TestNewTempServer(t *testing.T) { func TestNewTempServer(t *testing.T) {
defer ensure.HelmHome(t)() defer ensure.HelmHome(t)()
srv, err := NewTempServer("testdata/examplechart-0.1.0.tgz") srv, err := NewTempServerWithCleanup(t, "testdata/examplechart-0.1.0.tgz")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

@ -0,0 +1,50 @@
serverInfo:
contextPath: /v1/helm
apiVersion: v1
entries:
nginx:
- urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-0.2.0.tgz
name: nginx
description: string
version: 0.2.0
home: https://github.com/something/else
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
- urls:
- https://kubernetes-charts.storage.googleapis.com/nginx-0.1.0.tgz
name: nginx
description: string
version: 0.1.0
home: https://github.com/something
digest: "sha256:1234567890abcdef"
keywords:
- popular
- web server
- proxy
alpine:
- urls:
- https://kubernetes-charts.storage.googleapis.com/alpine-1.0.0.tgz
- http://storage2.googleapis.com/kubernetes-charts/alpine-1.0.0.tgz
name: alpine
description: string
version: 1.0.0
home: https://github.com/something
keywords:
- linux
- alpine
- small
- sumtin
digest: "sha256:1234567890abcdef"
chartWithNoURL:
- name: chartWithNoURL
description: string
version: 1.0.0
home: https://github.com/something
keywords:
- small
- sumtin
digest: "sha256:1234567890abcdef"

@ -19,8 +19,16 @@
: ${BINARY_NAME:="helm"} : ${BINARY_NAME:="helm"}
: ${USE_SUDO:="true"} : ${USE_SUDO:="true"}
: ${DEBUG:="false"}
: ${VERIFY_CHECKSUM:="true"}
: ${VERIFY_SIGNATURES:="false"}
: ${HELM_INSTALL_DIR:="/usr/local/bin"} : ${HELM_INSTALL_DIR:="/usr/local/bin"}
HAS_CURL="$(type "curl" &> /dev/null && echo true || echo false)"
HAS_WGET="$(type "wget" &> /dev/null && echo true || echo false)"
HAS_OPENSSL="$(type "openssl" &> /dev/null && echo true || echo false)"
HAS_GPG="$(type "gpg" &> /dev/null && echo true || echo false)"
# initArch discovers the architecture for this system. # initArch discovers the architecture for this system.
initArch() { initArch() {
ARCH=$(uname -m) ARCH=$(uname -m)
@ -58,7 +66,7 @@ runAsRoot() {
} }
# verifySupported checks that the os/arch combination is supported for # verifySupported checks that the os/arch combination is supported for
# binary builds. # binary builds, as well whether or not necessary tools are present.
verifySupported() { 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\nwindows-amd64"
if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then if ! echo "${supported}" | grep -q "${OS}-${ARCH}"; then
@ -67,10 +75,29 @@ verifySupported() {
exit 1 exit 1
fi fi
if ! type "curl" > /dev/null && ! type "wget" > /dev/null; then if [ "${HAS_CURL}" != "true" ] && [ "${HAS_WGET}" != "true" ]; then
echo "Either curl or wget is required" echo "Either curl or wget is required"
exit 1 exit 1
fi fi
if [ "${VERIFY_CHECKSUM}" == "true" ] && [ "${HAS_OPENSSL}" != "true" ]; then
echo "In order to verify checksum, openssl must first be installed."
echo "Please install openssl or set VERIFY_CHECKSUM=false in your environment."
exit 1
fi
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
if [ "${HAS_GPG}" != "true" ]; then
echo "In order to verify signatures, gpg must first be installed."
echo "Please install gpg or set VERIFY_SIGNATURES=false in your environment."
exit 1
fi
if [ "${OS}" != "linux" ]; then
echo "Signature verification is currently only supported on Linux."
echo "Please set VERIFY_SIGNATURES=false or verify the signatures manually."
exit 1
fi
fi
} }
# checkDesiredVersion checks if the desired version is available. # checkDesiredVersion checks if the desired version is available.
@ -78,9 +105,9 @@ checkDesiredVersion() {
if [ "x$DESIRED_VERSION" == "x" ]; then if [ "x$DESIRED_VERSION" == "x" ]; then
# Get tag from release URL # Get tag from release URL
local latest_release_url="https://github.com/helm/helm/releases" local latest_release_url="https://github.com/helm/helm/releases"
if type "curl" > /dev/null; then if [ "${HAS_CURL}" == "true" ]; then
TAG=$(curl -Ls $latest_release_url | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') TAG=$(curl -Ls $latest_release_url | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}')
elif type "wget" > /dev/null; then elif [ "${HAS_WGET}" == "true" ]; then
TAG=$(wget $latest_release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') TAG=$(wget $latest_release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v3.[0-9]*.[0-9]*\"' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}')
fi fi
else else
@ -115,35 +142,94 @@ downloadFile() {
HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST" HELM_TMP_FILE="$HELM_TMP_ROOT/$HELM_DIST"
HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256" HELM_SUM_FILE="$HELM_TMP_ROOT/$HELM_DIST.sha256"
echo "Downloading $DOWNLOAD_URL" echo "Downloading $DOWNLOAD_URL"
if type "curl" > /dev/null; then if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "$CHECKSUM_URL" -o "$HELM_SUM_FILE" 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" curl -SsL "$DOWNLOAD_URL" -o "$HELM_TMP_FILE"
elif type "wget" > /dev/null; then elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "$HELM_SUM_FILE" "$CHECKSUM_URL"
wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL" wget -q -O "$HELM_TMP_FILE" "$DOWNLOAD_URL"
fi fi
} }
# installFile verifies the SHA256 for the file, then unpacks and # verifyFile verifies the SHA256 checksum of the binary package
# installs it. # and the GPG signatures for both the package and checksum file
# (depending on settings in environment).
verifyFile() {
if [ "${VERIFY_CHECKSUM}" == "true" ]; then
verifyChecksum
fi
if [ "${VERIFY_SIGNATURES}" == "true" ]; then
verifySignatures
fi
}
# installFile installs the Helm binary.
installFile() { installFile() {
HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME" HELM_TMP="$HELM_TMP_ROOT/$BINARY_NAME"
mkdir -p "$HELM_TMP"
tar xf "$HELM_TMP_FILE" -C "$HELM_TMP"
HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm"
echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}"
runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME"
echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME"
}
# verifyChecksum verifies the SHA256 checksum of the binary package.
verifyChecksum() {
printf "Verifying checksum... "
local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}') local sum=$(openssl sha1 -sha256 ${HELM_TMP_FILE} | awk '{print $2}')
local expected_sum=$(cat ${HELM_SUM_FILE}) local expected_sum=$(cat ${HELM_SUM_FILE})
if [ "$sum" != "$expected_sum" ]; then if [ "$sum" != "$expected_sum" ]; then
echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting." echo "SHA sum of ${HELM_TMP_FILE} does not match. Aborting."
exit 1 exit 1
fi fi
echo "Done."
}
mkdir -p "$HELM_TMP" # verifySignatures obtains the latest KEYS file from GitHub master branch
tar xf "$HELM_TMP_FILE" -C "$HELM_TMP" # as well as the signature .asc files from the specific GitHub release,
HELM_TMP_BIN="$HELM_TMP/$OS-$ARCH/helm" # then verifies that the release artifacts were signed by a maintainer's key.
echo "Preparing to install $BINARY_NAME into ${HELM_INSTALL_DIR}" verifySignatures() {
runAsRoot cp "$HELM_TMP_BIN" "$HELM_INSTALL_DIR/$BINARY_NAME" printf "Verifying signatures... "
echo "$BINARY_NAME installed into $HELM_INSTALL_DIR/$BINARY_NAME" local keys_filename="KEYS"
local github_keys_url="https://raw.githubusercontent.com/helm/helm/master/${keys_filename}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_keys_url}" -o "${HELM_TMP_ROOT}/${keys_filename}"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "${HELM_TMP_ROOT}/${keys_filename}" "${github_keys_url}"
fi
local gpg_keyring="${HELM_TMP_ROOT}/keyring.gpg"
local gpg_homedir="${HELM_TMP_ROOT}/gnupg"
mkdir -p -m 0700 "${gpg_homedir}"
local gpg_stderr_device="/dev/null"
if [ "${DEBUG}" == "true" ]; then
gpg_stderr_device="/dev/stderr"
fi
gpg --batch --quiet --homedir="${gpg_homedir}" --import "${HELM_TMP_ROOT}/${keys_filename}" 2> "${gpg_stderr_device}"
gpg --batch --no-default-keyring --keyring "${gpg_homedir}/pubring.kbx" --export > "${gpg_keyring}"
local github_release_url="https://github.com/helm/helm/releases/download/${TAG}"
if [ "${HAS_CURL}" == "true" ]; then
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
curl -SsL "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" -o "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
elif [ "${HAS_WGET}" == "true" ]; then
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc"
wget -q -O "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" "${github_release_url}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc"
fi
local error_text="If you think this might be a potential security issue,"
error_text="${error_text}\nplease see here: https://github.com/helm/community/blob/master/SECURITY.md"
local num_goodlines_sha=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
if [[ ${num_goodlines_sha} -lt 2 ]]; then
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz.sha256!"
echo -e "${error_text}"
exit 1
fi
local num_goodlines_tar=$(gpg --verify --keyring="${gpg_keyring}" --status-fd=1 "${HELM_TMP_ROOT}/helm-${TAG}-${OS}-${ARCH}.tar.gz.asc" 2> "${gpg_stderr_device}" | grep -c -E '^\[GNUPG:\] (GOODSIG|VALIDSIG)')
if [[ ${num_goodlines_tar} -lt 2 ]]; then
echo "Unable to verify the signature of helm-${TAG}-${OS}-${ARCH}.tar.gz!"
echo -e "${error_text}"
exit 1
fi
echo "Done."
} }
# fail_trap is executed if an error occurs. # fail_trap is executed if an error occurs.
@ -195,6 +281,11 @@ cleanup() {
trap "fail_trap" EXIT trap "fail_trap" EXIT
set -e set -e
# Set debug if desired
if [ "${DEBUG}" == "true" ]; then
set -x
fi
# Parsing input arguments (if any) # Parsing input arguments (if any)
export INPUT_ARGUMENTS="${@}" export INPUT_ARGUMENTS="${@}"
set -u set -u
@ -229,6 +320,7 @@ verifySupported
checkDesiredVersion checkDesiredVersion
if ! checkHelmInstalledVersion; then if ! checkHelmInstalledVersion; then
downloadFile downloadFile
verifyFile
installFile installFile
fi fi
testVersion testVersion

Loading…
Cancel
Save