Merge branch 'master' of github.com:helm/helm into helm-7351

pull/7356/head
EItanya 5 years ago
commit 872d9bcb0e

2
.gitignore vendored

@ -7,3 +7,5 @@
_dist/
bin/
vendor/
# Ignores charts pulled for dependency build tests
cmd/helm/testdata/testcharts/issue-7233/charts/*

@ -17,6 +17,7 @@ linters:
- structcheck
- unused
- varcheck
- staticcheck
linters-settings:
gofmt:

@ -0,0 +1,15 @@
To add your organization to this list, open a pull request that adds your
organization's name, optionally with a link. The list is in alphabetical order.
(Remember to use `git commit --signoff` to comply with the DCO)
# Organizations Using Helm
- [Blood Orange](https://bloodorange.io)
- [IBM](https://www.ibm.com)
- [Microsoft](https://microsoft.com)
- [Qovery](https://www.qovery.com/)
- [Samsung SDS](https://www.samsungsds.com/)
- [Ville de Montreal](https://montreal.ca)
_This file is part of the CNCF official documentation for projects._

@ -1,15 +1,16 @@
BINDIR := $(CURDIR)/bin
DIST_DIRS := find * -type d -exec
TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64
TARGET_OBJS ?= darwin-amd64.tar.gz darwin-amd64.tar.gz.sha256 linux-amd64.tar.gz linux-amd64.tar.gz.sha256 linux-386.tar.gz linux-386.tar.gz.sha256 linux-arm.tar.gz linux-arm.tar.gz.sha256 linux-arm64.tar.gz linux-arm64.tar.gz.sha256 linux-ppc64le.tar.gz linux-ppc64le.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256
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
BINNAME ?= helm
GOPATH = $(shell go env GOPATH)
DEP = $(GOPATH)/bin/dep
GOX = $(GOPATH)/bin/gox
GOIMPORTS = $(GOPATH)/bin/goimports
ARCH = $(shell uname -p)
ACCEPTANCE_DIR:=$(GOPATH)/src/helm.sh/acceptance-testing
ACCEPTANCE_DIR:=../acceptance-testing
# To specify the subset of acceptance tests to run. '.' means all tests
ACCEPTANCE_RUN_TESTS=.
@ -23,7 +24,7 @@ GOFLAGS :=
SRC := $(shell find . -type f -name '*.go' -print)
# Required for globs to work correctly
SHELL = /bin/bash
SHELL = /usr/bin/env bash
GIT_COMMIT = $(shell git rev-parse HEAD)
GIT_SHA = $(shell git rev-parse --short HEAD)
@ -40,10 +41,13 @@ ifneq ($(BINARY_VERSION),)
LDFLAGS += -X helm.sh/helm/v3/internal/version.version=${BINARY_VERSION}
endif
VERSION_METADATA = unreleased
# Clear the "unreleased" string in BuildMetadata
ifneq ($(GIT_TAG),)
LDFLAGS += -X helm.sh/helm/v3/internal/version.metadata=
VERSION_METADATA =
endif
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.gitTreeState=${GIT_DIRTY}
@ -64,7 +68,11 @@ $(BINDIR)/$(BINNAME): $(SRC)
.PHONY: test
test: build
ifeq ($(ARCH),s390x)
test: TESTFLAGS += -v
else
test: TESTFLAGS += -race -v
endif
test: test-style
test: test-unit
@ -149,14 +157,22 @@ fetch-dist:
.PHONY: sign
sign:
for f in _dist/*.{gz,zip,sha256} ; do \
for f in _dist/*.{gz,zip,sha256,sha256sum} ; do \
gpg --armor --detach-sign $${f} ; \
done
# The contents of the .sha256sum file are compatible with tools like
# shasum. For example, using the following command will verify
# the file helm-3.1.0-rc.1-darwin-amd64.tar.gz:
# shasum -a 256 -c helm-3.1.0-rc.1-darwin-amd64.tar.gz.sha256sum
# The .sha256 files hold only the hash and are not compatible with
# verification tools like shasum or sha256sum. This method and file can be
# removed in Helm v4.
.PHONY: checksum
checksum:
for f in _dist/*.{gz,zip} ; do \
shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \
shasum -a 256 "$${f}" | sed 's/_dist\///' > "$${f}.sha256sum" ; \
shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \
done
# ------------------------------------------------------------------------------

@ -0,0 +1,3 @@
# Helm Security Reporting and Policy
The Helm project has [a common process and policy that can be found here](https://github.com/helm/community/blob/master/SECURITY.md).

@ -139,7 +139,10 @@ __helm_compgen() {
fi
for w in "${completions[@]}"; do
if [[ "${w}" = "$1"* ]]; then
echo "${w}"
# Use printf instead of echo beause it is possible that
# the value to print is -n, which would be interpreted
# as a flag to echo
printf "%s\n" "${w}"
fi
done
}

@ -100,3 +100,16 @@ func TestDependencyBuildCmd(t *testing.T) {
t.Errorf("mismatched versions. Expected %q, got %q", "0.1.0", v)
}
}
func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) {
chartName := "testdata/testcharts/issue-7233"
cmd := fmt.Sprintf("dependency build '%s'", chartName)
_, out, err := executeActionCommand(cmd)
// Want to make sure the build can verify Helm v2 hash
if err != nil {
t.Logf("Output: %s", out)
t.Fatal(err)
}
}

@ -23,12 +23,15 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/postrender"
)
const outputFlag = "output"
const postRenderFlag = "post-renderer"
func addValueOptionsFlags(f *pflag.FlagSet, v *values.Options) {
f.StringSliceVarP(&v.ValueFiles, "values", "f", []string{}, "specify values in a YAML file or a URL (can specify multiple)")
@ -52,10 +55,19 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
// bindOutputFlag will add the output flag to the given command and bind the
// value to the given format pointer
func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o",
f := cmd.Flags()
flag := f.VarPF(newOutputValue(output.Table, varRef), outputFlag, "o",
fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", ")))
// Setup shell completion for the flag
cmd.MarkFlagCustom(outputFlag, "__helm_output_options")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
var formatNames []string
for _, format := range output.Formats() {
if strings.HasPrefix(format, toComplete) {
formatNames = append(formatNames, format)
}
}
return formatNames, completion.BashCompDirectiveDefault
})
}
type outputValue output.Format
@ -84,3 +96,31 @@ func (o *outputValue) Set(s string) error {
*o = outputValue(outfmt)
return nil
}
func bindPostRenderFlag(cmd *cobra.Command, varRef *postrender.PostRenderer) {
cmd.Flags().Var(&postRenderer{varRef}, postRenderFlag, "the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path")
}
type postRenderer struct {
renderer *postrender.PostRenderer
}
func (p postRenderer) String() string {
return "exec"
}
func (p postRenderer) Type() string {
return "postrenderer"
}
func (p postRenderer) Set(s string) error {
if s == "" {
return nil
}
pr, err := postrender.NewExec(s)
if err != nil {
return err
}
*p.renderer = pr
return nil
}

@ -0,0 +1,88 @@
/*
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"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
func outputFlagCompletionTest(t *testing.T, cmdName string) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "athos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "porthos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "aramis",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "dartagnan",
Namespace: "gascony",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}}
}
tests := []cmdTestCase{{
name: "completion for output flag long and before arg",
cmd: fmt.Sprintf("__complete %s --output ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag long and after arg",
cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag short and before arg",
cmd: fmt.Sprintf("__complete %s -o ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag short and after arg",
cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}}
runTestCmd(t, tests)
}

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
@ -56,8 +57,24 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
f.StringVar(&template, "template", "", "go template for formatting the output, eg: {{.Release.Name}}")
return cmd

@ -41,3 +41,7 @@ func TestGetCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetAllRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get all")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -52,7 +53,23 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision")
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -36,3 +36,7 @@ func TestGetHooks(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetHooksRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get hooks")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -52,7 +53,23 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
},
}
cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision")
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -36,3 +36,7 @@ func TestGetManifest(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetManifestRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get manifest")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -50,8 +51,23 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -36,3 +36,7 @@ func TestGetNotesCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetNotesRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get notes")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
@ -54,8 +55,23 @@ func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
f.BoolVarP(&client.AllValues, "all", "a", false, "dump all (computed) values")
bindOutputFlag(cmd, &outfmt)

@ -52,3 +52,11 @@ func TestGetValuesCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetValuesRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get values")
}
func TestGetValuesOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "get values")
}

@ -19,6 +19,7 @@ package main // import "helm.sh/helm/v3/cmd/helm"
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
@ -26,13 +27,18 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"k8s.io/klog"
"sigs.k8s.io/yaml"
// Import to initialize client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli"
"helm.sh/helm/v3/pkg/gates"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage/driver"
)
// FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work
@ -71,9 +77,22 @@ func main() {
os.Exit(1)
}
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil {
debug("%+v", err)
os.Exit(1)
if calledCmd, _, err := cmd.Find(os.Args[1:]); err == nil && calledCmd.Name() == completion.CompRequestCmd {
// If completion is being called, we have to check if the completion is for the "--kube-context"
// value; if it is, we cannot call the action.Init() method with an incomplete kube-context value
// or else it will fail immediately. So, we simply unset the invalid kube-context value.
if args := os.Args[1:]; len(args) > 2 && args[len(args)-2] == "--kube-context" {
// We are completing the kube-context value! Reset it as the current value is not valid.
settings.KubeContext = ""
}
}
helmDriver := os.Getenv("HELM_DRIVER")
if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil {
log.Fatal(err)
}
if helmDriver == "memory" {
loadReleasesInMemory(actionConfig)
}
if err := cmd.Execute(); err != nil {
@ -100,3 +119,41 @@ func checkOCIFeatureGate() func(_ *cobra.Command, _ []string) error {
return nil
}
}
// This function loads releases into the memory storage if the
// environment variable is properly set.
func loadReleasesInMemory(actionConfig *action.Configuration) {
filePaths := strings.Split(os.Getenv("HELM_MEMORY_DRIVER_DATA"), ":")
if len(filePaths) == 0 {
return
}
store := actionConfig.Releases
mem, ok := store.Driver.(*driver.Memory)
if !ok {
// For an unexpected reason we are not dealing with the memory storage driver.
return
}
actionConfig.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}
for _, path := range filePaths {
b, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal("Unable to read memory driver data", err)
}
releases := []*release.Release{}
if err := yaml.Unmarshal(b, &releases); err != nil {
log.Fatal("Unable to unmarshal memory driver data: ", err)
}
for _, rel := range releases {
if err := store.Create(rel); err != nil {
log.Fatal(err)
}
}
}
// Must reset namespace to the proper one
mem.SetNamespace(settings.Namespace())
}

@ -31,6 +31,7 @@ import (
"helm.sh/helm/v3/internal/test"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli"
kubefake "helm.sh/helm/v3/pkg/kube/fake"
"helm.sh/helm/v3/pkg/release"
"helm.sh/helm/v3/pkg/storage"
@ -47,24 +48,26 @@ func init() {
func runTestCmd(t *testing.T, tests []cmdTestCase) {
t.Helper()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
storage := storageFixture()
for _, rel := range tt.rels {
if err := storage.Create(rel); err != nil {
t.Fatal(err)
for i := 0; i <= tt.repeat; i++ {
t.Run(tt.name, func(t *testing.T) {
defer resetEnv()()
storage := storageFixture()
for _, rel := range tt.rels {
if err := storage.Create(rel); err != nil {
t.Fatal(err)
}
}
}
t.Log("running cmd: ", tt.cmd)
_, out, err := executeActionCommandC(storage, tt.cmd)
if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
}
})
t.Logf("running cmd (attempt %d): %s", i+1, tt.cmd)
_, out, err := executeActionCommandC(storage, tt.cmd)
if (err != nil) != tt.wantError {
t.Errorf("expected error, got '%v'", err)
}
if tt.golden != "" {
test.AssertGoldenString(t, out, tt.golden)
}
})
}
}
}
@ -115,6 +118,9 @@ func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command,
root.SetOutput(buf)
root.SetArgs(args)
if mem, ok := store.Driver.(*driver.Memory); ok {
mem.SetNamespace(settings.Namespace())
}
c, err := root.ExecuteC()
return c, buf.String(), err
@ -128,6 +134,9 @@ type cmdTestCase struct {
wantError bool
// Rels are the available releases at the start of the test.
rels []*release.Release
// Number of repeats (in case a feature was previously flaky and the test checks
// it's now stably producing identical results). 0 means test is run exactly once.
repeat int
}
func executeActionCommand(cmd string) (*cobra.Command, string, error) {
@ -135,14 +144,14 @@ func executeActionCommand(cmd string) (*cobra.Command, string, error) {
}
func resetEnv() func() {
origSettings, origEnv := settings, os.Environ()
origEnv := os.Environ()
return func() {
os.Clearenv()
settings = origSettings
for _, pair := range origEnv {
kv := strings.SplitN(pair, "=", 2)
os.Setenv(kv[0], kv[1])
}
settings = cli.New()
}
}

@ -19,12 +19,14 @@ package main
import (
"fmt"
"io"
"strconv"
"time"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/cli/output"
@ -69,6 +71,14 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history")
bindOutputFlag(cmd, &outfmt)
@ -176,3 +186,16 @@ func min(x, y int) int {
}
return y
}
func compListRevisions(cfg *action.Configuration, releaseName string) ([]string, completion.BashCompDirective) {
client := action.NewHistory(cfg)
var revisions []string
if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist {
revisions = append(revisions, strconv.Itoa(release.Version))
}
return revisions, completion.BashCompDirectiveDefault
}
return nil, completion.BashCompDirectiveError
}

@ -17,6 +17,7 @@ limitations under the License.
package main
import (
"fmt"
"testing"
"helm.sh/helm/v3/pkg/release"
@ -68,3 +69,42 @@ func TestHistoryCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestHistoryOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "history")
}
func revisionFlagCompletionTest(t *testing.T, cmdName string) {
mk := func(name string, vers int, status release.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,
Version: vers,
Status: status,
})
}
releases := []*release.Release{
mk("musketeers", 11, release.StatusDeployed),
mk("musketeers", 10, release.StatusSuperseded),
mk("musketeers", 9, release.StatusSuperseded),
mk("musketeers", 8, release.StatusSuperseded),
}
tests := []cmdTestCase{{
name: "completion for revision flag",
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag with too few args",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),
rels: releases,
golden: "output/revision-wrong-args-comp.txt",
}, {
name: "completion for revision flag with too many args",
cmd: fmt.Sprintf("__complete %s three musketeers --revision ''", cmdName),
rels: releases,
golden: "output/revision-wrong-args-comp.txt",
}}
runTestCmd(t, tests)
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/pflag"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
@ -122,13 +123,20 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
return compInstall(args, toComplete, client)
})
addInstallFlags(cmd.Flags(), client, valueOpts)
bindOutputFlag(cmd, &outfmt)
bindPostRenderFlag(cmd, &client.PostRenderer)
return cmd
}
func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) {
f.BoolVar(&client.CreateNamespace, "create-namespace", false, "create the release namespace if not present")
f.BoolVar(&client.DryRun, "dry-run", false, "simulate an install")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during install")
f.BoolVar(&client.Replace, "replace", false, "re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production")
@ -139,6 +147,7 @@ func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values
f.StringVar(&client.Description, "description", "", "add a custom description")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
f.BoolVar(&client.DependencyUpdate, "dependency-update", false, "run helm dependency update before installing the chart")
f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema")
f.BoolVar(&client.Atomic, "atomic", false, "if set, installation process purges chart on fail. The --wait flag will be set automatically if --atomic is used")
f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed. By default, CRDs are installed if not already present")
f.BoolVar(&client.SubNotes, "render-subchart-notes", false, "if set, render subchart notes along with the parent")
@ -225,3 +234,15 @@ func isChartInstallable(ch *chart.Chart) (bool, error) {
}
return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}
// Provide dynamic auto-completion for the install and template commands
func compInstall(args []string, toComplete string, client *action.Install) ([]string, completion.BashCompDirective) {
requiredArgs := 1
if client.GenerateName {
requiredArgs = 0
}
if len(args) == requiredArgs {
return compListCharts(toComplete, true)
}
return nil, completion.BashCompDirectiveNoFileComp
}

@ -193,3 +193,7 @@ func TestInstall(t *testing.T) {
runTestActionCmd(t, tests)
}
func TestInstallOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "install")
}

@ -19,6 +19,8 @@ package main
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/pkg/errors"
@ -51,6 +53,21 @@ func newLintCmd(out io.Writer) *cobra.Command {
if len(args) > 0 {
paths = args
}
if client.WithSubcharts {
for _, p := range paths {
filepath.Walk(filepath.Join(p, "charts"), func(path string, info os.FileInfo, err error) error {
if info != nil {
if info.Name() == "Chart.yaml" {
paths = append(paths, filepath.Dir(path))
} else if strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") {
paths = append(paths, path)
}
}
return nil
})
}
}
client.Namespace = settings.Namespace()
vals, err := valueOpts.MergeValues(getter.All(settings))
if err != nil {
@ -89,7 +106,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
fmt.Fprint(&message, "\n")
}
fmt.Fprintf(out, message.String())
fmt.Fprint(out, message.String())
summary := fmt.Sprintf("%d chart(s) linted, %d chart(s) failed", len(paths), failed)
if failed > 0 {
@ -102,6 +119,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
f := cmd.Flags()
f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
addValueOptionsFlags(f, valueOpts)
return cmd

@ -0,0 +1,38 @@
/*
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"
"testing"
)
func TestLintCmdWithSubchartsFlag(t *testing.T) {
testChart := "testdata/testcharts/chart-with-bad-subcharts"
tests := []cmdTestCase{{
name: "lint good chart with bad subcharts",
cmd: fmt.Sprintf("lint %s", testChart),
golden: "output/lint-chart-with-bad-subcharts.txt",
wantError: false,
}, {
name: "lint good chart with bad subcharts using --with-subcharts flag",
cmd: fmt.Sprintf("lint --with-subcharts %s", testChart),
golden: "output/lint-chart-with-bad-subcharts-with-subcharts.txt",
wantError: true,
}}
runTestCmd(t, tests)
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/release"
@ -164,3 +165,26 @@ func (r *releaseListWriter) WriteJSON(out io.Writer) error {
func (r *releaseListWriter) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, r.releases)
}
// Provide dynamic auto-completion for release names
func compListReleases(toComplete string, cfg *action.Configuration) ([]string, completion.BashCompDirective) {
completion.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete))
client := action.NewList(cfg)
client.All = true
client.Limit = 0
client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask()
results, err := client.Run()
if err != nil {
return nil, completion.BashCompDirectiveDefault
}
var choices []string
for _, res := range results {
choices = append(choices, res.Name)
}
return choices, completion.BashCompDirectiveNoFileComp
}

@ -131,6 +131,16 @@ func TestListCmd(t *testing.T) {
},
Chart: chartInfo,
},
{
Name: "starlord",
Version: 2,
Namespace: "milano",
Info: &release.Info{
LastDeployed: timestamp1,
Status: release.StatusDeployed,
},
Chart: chartInfo,
},
}
tests := []cmdTestCase{{
@ -203,6 +213,15 @@ func TestListCmd(t *testing.T) {
cmd: "list --uninstalling",
golden: "output/list-uninstalling.txt",
rels: releaseFixture,
}, {
name: "list releases in another namespace",
cmd: "list -n milano",
golden: "output/list-namespace.txt",
rels: releaseFixture,
}}
runTestCmd(t, tests)
}
func TestListOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "list")
}

@ -16,20 +16,31 @@ limitations under the License.
package main
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/plugin"
)
const (
pluginStaticCompletionFile = "completion.yaml"
pluginDynamicCompletionExecutable = "plugin.complete"
)
type pluginError struct {
error
code int
@ -61,6 +72,13 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return u, nil
}
// If we are dealing with the completion command, we try to load more details about the plugins
// if available, so as to allow for command and flag completion
if subCmd, _, err := baseCmd.Find(os.Args[1:]); err == nil && subCmd.Name() == "completion" {
loadPluginsForCompletion(baseCmd, found)
return
}
// Now we create commands for all of these.
for _, plug := range found {
plug := plug
@ -69,6 +87,33 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
md.Usage = fmt.Sprintf("the %q plugin", md.Name)
}
// This function is used to setup the environment for the plugin and then
// call the executable specified by the parameter 'main'
callPluginExecutable := func(cmd *cobra.Command, main string, argv []string, out io.Writer) error {
env := os.Environ()
for k, v := range settings.EnvVars() {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
prog := exec.Command(main, argv...)
prog.Env = env
prog.Stdin = os.Stdin
prog.Stdout = out
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
status := eerr.Sys().(syscall.WaitStatus)
return pluginError{
error: errors.Errorf("plugin %q exited with error", md.Name),
code: status.ExitStatus(),
}
}
return err
}
return nil
}
c := &cobra.Command{
Use: md.Name,
Short: md.Usage,
@ -89,33 +134,59 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
return errors.Errorf("plugin %q exited with error", md.Name)
}
env := os.Environ()
for k, v := range settings.EnvVars() {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
prog := exec.Command(main, argv...)
prog.Env = env
prog.Stdin = os.Stdin
prog.Stdout = out
prog.Stderr = os.Stderr
if err := prog.Run(); err != nil {
if eerr, ok := err.(*exec.ExitError); ok {
os.Stderr.Write(eerr.Stderr)
status := eerr.Sys().(syscall.WaitStatus)
return pluginError{
error: errors.Errorf("plugin %q exited with error", md.Name),
code: status.ExitStatus(),
}
}
return err
}
return nil
return callPluginExecutable(cmd, main, argv, out)
},
// This passes all the flags to the subcommand.
DisableFlagParsing: true,
}
// Setup dynamic completion for the plugin
completion.RegisterValidArgsFunc(c, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
u, err := processParent(cmd, args)
if err != nil {
return nil, completion.BashCompDirectiveError
}
// We will call the dynamic completion script of the plugin
main := strings.Join([]string{plug.Dir, pluginDynamicCompletionExecutable}, string(filepath.Separator))
argv := []string{}
if !md.IgnoreFlags {
argv = append(argv, u...)
argv = append(argv, toComplete)
}
plugin.SetupPluginEnv(settings, md.Name, plug.Dir)
completion.CompDebugln(fmt.Sprintf("calling %s with args %v", main, argv))
buf := new(bytes.Buffer)
if err := callPluginExecutable(cmd, main, argv, buf); err != nil {
return nil, completion.BashCompDirectiveError
}
var completions []string
for _, comp := range strings.Split(buf.String(), "\n") {
// Remove any empty lines
if len(comp) > 0 {
completions = append(completions, comp)
}
}
// Check if the last line of output is of the form :<integer>, which
// indicates the BashCompletionDirective.
directive := completion.BashCompDirectiveDefault
if len(completions) > 0 {
lastLine := completions[len(completions)-1]
if len(lastLine) > 1 && lastLine[0] == ':' {
if strInt, err := strconv.Atoi(lastLine[1:]); err == nil {
directive = completion.BashCompDirective(strInt)
completions = completions[:len(completions)-1]
}
}
}
return completions, directive
})
// TODO: Make sure a command with this name does not already exist.
baseCmd.AddCommand(c)
}
@ -127,7 +198,7 @@ func loadPlugins(baseCmd *cobra.Command, out io.Writer) {
func manuallyProcessArgs(args []string) ([]string, []string) {
known := []string{}
unknown := []string{}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--registry-config", "--repository-cache", "--repository-config"}
kvargs := []string{"--kube-context", "--namespace", "-n", "--kubeconfig", "--kube-apiserver", "--kube-token", "--registry-config", "--repository-cache", "--repository-config"}
knownArg := func(a string) bool {
for _, pre := range kvargs {
if strings.HasPrefix(a, pre+"=") {
@ -180,3 +251,119 @@ func findPlugins(plugdirs string) ([]*plugin.Plugin, error) {
}
return found, nil
}
// pluginCommand represents the optional completion.yaml file of a plugin
type pluginCommand struct {
Name string `json:"name"`
ValidArgs []string `json:"validArgs"`
Flags []string `json:"flags"`
Commands []pluginCommand `json:"commands"`
}
// loadPluginsForCompletion will load and parse any completion.yaml provided by the plugins
func loadPluginsForCompletion(baseCmd *cobra.Command, plugins []*plugin.Plugin) {
for _, plug := range plugins {
// Parse the yaml file providing the plugin's subcmds and flags
cmds, err := loadFile(strings.Join(
[]string{plug.Dir, pluginStaticCompletionFile}, string(filepath.Separator)))
if err != nil {
// The file could be missing or invalid. Either way, we at least create the command
// for the plugin name.
if settings.Debug {
log.Output(2, fmt.Sprintf("[info] %s\n", err.Error()))
}
cmds = &pluginCommand{Name: plug.Metadata.Name}
}
// We know what the plugin name must be.
// Let's set it in case the Name field was not specified correctly in the file.
// This insures that we will at least get the plugin name to complete, even if
// there is a problem with the completion.yaml file
cmds.Name = plug.Metadata.Name
addPluginCommands(baseCmd, cmds)
}
}
// addPluginCommands is a recursive method that adds the different levels
// of sub-commands and flags for the plugins that provide such information
func addPluginCommands(baseCmd *cobra.Command, cmds *pluginCommand) {
if cmds == nil {
return
}
if len(cmds.Name) == 0 {
// Missing name for a command
if settings.Debug {
log.Output(2, fmt.Sprintf("[info] sub-command name field missing for %s", baseCmd.CommandPath()))
}
return
}
// Create a fake command just so the completion script will include it
c := &cobra.Command{
Use: cmds.Name,
ValidArgs: cmds.ValidArgs,
// A Run is required for it to be a valid command without subcommands
Run: func(cmd *cobra.Command, args []string) {},
}
baseCmd.AddCommand(c)
// Create fake flags.
if len(cmds.Flags) > 0 {
// The flags can be created with any type, since we only need them for completion.
// pflag does not allow to create short flags without a corresponding long form
// so we look for all short flags and match them to any long flag. This will allow
// plugins to provide short flags without a long form.
// If there are more short-flags than long ones, we'll create an extra long flag with
// the same single letter as the short form.
shorts := []string{}
longs := []string{}
for _, flag := range cmds.Flags {
if len(flag) == 1 {
shorts = append(shorts, flag)
} else {
longs = append(longs, flag)
}
}
f := c.Flags()
if len(longs) >= len(shorts) {
for i := range longs {
if i < len(shorts) {
f.BoolP(longs[i], shorts[i], false, "")
} else {
f.Bool(longs[i], false, "")
}
}
} else {
for i := range shorts {
if i < len(longs) {
f.BoolP(longs[i], shorts[i], false, "")
} else {
// Create a long flag with the same name as the short flag.
// Not a perfect solution, but its better than ignoring the extra short flags.
f.BoolP(shorts[i], shorts[i], false, "")
}
}
}
}
// Recursively add any sub-commands
for _, cmd := range cmds.Commands {
addPluginCommands(c, &cmd)
}
}
// loadFile takes a yaml file at the given path, parses it and returns a pluginCommand object
func loadFile(path string) (*pluginCommand, error) {
cmds := new(pluginCommand)
b, err := ioutil.ReadFile(path)
if err != nil {
return cmds, errors.New(fmt.Sprintf("File (%s) not provided by plugin. No plugin auto-completion possible.", path))
}
err = yaml.Unmarshal(b, cmds)
return cmds, err
}

@ -20,6 +20,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/pkg/errors"
@ -80,6 +81,9 @@ func newPackageCmd(out io.Writer) *cobra.Command {
if err != nil {
return err
}
if _, err := os.Stat(args[i]); err != nil {
return err
}
if client.DependencyUpdate {
downloadManager := &downloader.Manager{
@ -114,7 +118,6 @@ func newPackageCmd(out io.Writer) *cobra.Command {
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.BoolVarP(&client.DependencyUpdate, "dependency-update", "u", false, `update dependencies from "Chart.yaml" to dir "charts/" before packaging`)
addValueOptionsFlags(f, valueOpts)
return cmd
}

@ -17,13 +17,9 @@ package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
"github.com/spf13/cobra"
@ -31,15 +27,9 @@ import (
"helm.sh/helm/v3/internal/test/ensure"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/chartutil"
)
func TestPackage(t *testing.T) {
statFileMsg := "no such file or directory"
if runtime.GOOS == "windows" {
statFileMsg = "The system cannot find the file specified."
}
tests := []struct {
name string
flags map[string]string
@ -108,13 +98,6 @@ func TestPackage(t *testing.T) {
hasfile: "chart-missing-deps-0.1.0.tgz",
err: true,
},
{
name: "package --values does-not-exist",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"values": "does-not-exist"},
expect: fmt.Sprintf("does-not-exist: %s", statFileMsg),
err: true,
},
{
name: "package testdata/testcharts/chart-bad-type",
args: []string{"testdata/testcharts/chart-bad-type"},
@ -213,124 +196,6 @@ func TestSetAppVersion(t *testing.T) {
}
}
func TestPackageValues(t *testing.T) {
defer resetEnv()()
repoFile := "testdata/helmhome/helm/repositories.yaml"
testCases := []struct {
desc string
args []string
valuefilesContents []string
flags map[string]string
expected []string
}{
{
desc: "helm package, single values file",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"repository-config": repoFile},
valuefilesContents: []string{"Name: chart-name-foo"},
expected: []string{"Name: chart-name-foo"},
},
{
desc: "helm package, multiple values files",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"repository-config": repoFile},
valuefilesContents: []string{"Name: chart-name-foo", "foo: bar"},
expected: []string{"Name: chart-name-foo", "foo: bar"},
},
{
desc: "helm package, with set option",
args: []string{"testdata/testcharts/alpine"},
flags: map[string]string{"set": "Name=chart-name-foo", "repository-config": repoFile},
expected: []string{"Name: chart-name-foo"},
},
{
desc: "helm package, set takes precedence over value file",
args: []string{"testdata/testcharts/alpine"},
valuefilesContents: []string{"Name: chart-name-foo"},
flags: map[string]string{"set": "Name=chart-name-bar", "repository-config": repoFile},
expected: []string{"Name: chart-name-bar"},
},
}
for _, tc := range testCases {
var files []string
for _, contents := range tc.valuefilesContents {
f := createValuesFile(t, contents)
files = append(files, f)
}
valueFiles := strings.Join(files, ",")
expected, err := chartutil.ReadValues([]byte(strings.Join(tc.expected, "\n")))
if err != nil {
t.Errorf("unexpected error parsing values: %q", err)
}
outputDir := ensure.TempDir(t)
if len(tc.flags) == 0 {
tc.flags = make(map[string]string)
}
tc.flags["destination"] = outputDir
if len(valueFiles) > 0 {
tc.flags["values"] = valueFiles
}
cmd := newPackageCmd(&bytes.Buffer{})
setFlags(cmd, tc.flags)
if err := cmd.RunE(cmd, tc.args); err != nil {
t.Fatalf("unexpected error: %q", err)
}
outputFile := filepath.Join(outputDir, "alpine-0.1.0.tgz")
verifyOutputChartExists(t, outputFile)
actual, err := getChartValues(outputFile)
if err != nil {
t.Fatalf("unexpected error extracting chart values: %q", err)
}
verifyValues(t, actual, expected)
}
}
func createValuesFile(t *testing.T, data string) string {
outputDir := ensure.TempDir(t)
outputFile := filepath.Join(outputDir, "values.yaml")
if err := ioutil.WriteFile(outputFile, []byte(data), 0644); err != nil {
t.Fatalf("err: %s", err)
}
return outputFile
}
func getChartValues(chartPath string) (chartutil.Values, error) {
chart, err := loader.Load(chartPath)
if err != nil {
return nil, err
}
return chart.Values, nil
}
func verifyValues(t *testing.T, actual, expected chartutil.Values) {
t.Helper()
for key, value := range expected.AsMap() {
if got := actual[key]; got != value {
t.Errorf("Expected %q, got %q (%v)", value, got, actual)
}
}
}
func verifyOutputChartExists(t *testing.T, chartPath string) {
if chartFile, err := os.Stat(chartPath); err != nil {
t.Errorf("expected file %q, got err %q", chartPath, err)
} else if chartFile.Size() == 0 {
t.Errorf("file %q has zero bytes.", chartPath)
}
}
func setFlags(cmd *cobra.Command, flags map[string]string) {
dest := cmd.Flags()
for f, v := range flags {

@ -18,6 +18,7 @@ package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
@ -46,3 +47,17 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
}
return cmd
}
// Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string) []string {
var pNames []string
plugins, err := findPlugins(settings.PluginsDirectory)
if err == nil {
for _, p := range plugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) {
pNames = append(pNames, p.Metadata.Name)
}
}
}
return pNames
}

@ -19,10 +19,14 @@ import (
"bytes"
"os"
"runtime"
"sort"
"strings"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/pkg/release"
)
func TestManuallyProcessArgs(t *testing.T) {
@ -151,6 +155,134 @@ func TestLoadPlugins(t *testing.T) {
}
}
type staticCompletionDetails struct {
use string
validArgs []string
flags []string
next []staticCompletionDetails
}
func TestLoadPluginsForCompletion(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
var out bytes.Buffer
cmd := &cobra.Command{
Use: "completion",
}
loadPlugins(cmd, &out)
tests := []staticCompletionDetails{
{"args", []string{}, []string{}, []staticCompletionDetails{}},
{"echo", []string{}, []string{}, []staticCompletionDetails{}},
{"env", []string{}, []string{"global"}, []staticCompletionDetails{
{"list", []string{}, []string{"a", "all", "log"}, []staticCompletionDetails{}},
{"remove", []string{"all", "one"}, []string{}, []staticCompletionDetails{}},
}},
{"exitwith", []string{}, []string{}, []staticCompletionDetails{
{"code", []string{}, []string{"a", "b"}, []staticCompletionDetails{}},
}},
{"fullenv", []string{}, []string{"q", "z"}, []staticCompletionDetails{
{"empty", []string{}, []string{}, []staticCompletionDetails{}},
{"full", []string{}, []string{}, []staticCompletionDetails{
{"less", []string{}, []string{"a", "all"}, []staticCompletionDetails{}},
{"more", []string{"one", "two"}, []string{"b", "ball"}, []staticCompletionDetails{}},
}},
}},
}
checkCommand(t, cmd.Commands(), tests)
}
func checkCommand(t *testing.T, plugins []*cobra.Command, tests []staticCompletionDetails) {
if len(plugins) != len(tests) {
t.Fatalf("Expected commands %v, got %v", tests, plugins)
}
for i := 0; i < len(plugins); i++ {
pp := plugins[i]
tt := tests[i]
if pp.Use != tt.use {
t.Errorf("%s: Expected Use=%q, got %q", pp.Name(), tt.use, pp.Use)
}
targs := tt.validArgs
pargs := pp.ValidArgs
if len(targs) != len(pargs) {
t.Fatalf("%s: expected args %v, got %v", pp.Name(), targs, pargs)
}
sort.Strings(targs)
sort.Strings(pargs)
for j := range targs {
if targs[j] != pargs[j] {
t.Errorf("%s: expected validArg=%q, got %q", pp.Name(), targs[j], pargs[j])
}
}
tflags := tt.flags
var pflags []string
pp.LocalFlags().VisitAll(func(flag *pflag.Flag) {
pflags = append(pflags, flag.Name)
if len(flag.Shorthand) > 0 && flag.Shorthand != flag.Name {
pflags = append(pflags, flag.Shorthand)
}
})
if len(tflags) != len(pflags) {
t.Fatalf("%s: expected flags %v, got %v", pp.Name(), tflags, pflags)
}
sort.Strings(tflags)
sort.Strings(pflags)
for j := range tflags {
if tflags[j] != pflags[j] {
t.Errorf("%s: expected flag=%q, got %q", pp.Name(), tflags[j], pflags[j])
}
}
// Check the next level
checkCommand(t, pp.Commands(), tt.next)
}
}
func TestPluginDynamicCompletion(t *testing.T) {
tests := []cmdTestCase{{
name: "completion for plugin",
cmd: "__complete args ''",
golden: "output/plugin_args_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with flag",
cmd: "__complete args --myflag ''",
golden: "output/plugin_args_flag_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with global flag",
cmd: "__complete args --namespace mynamespace ''",
golden: "output/plugin_args_ns_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin with multiple args",
cmd: "__complete args --myflag --namespace mynamespace start",
golden: "output/plugin_args_many_args_comp.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin no directive",
cmd: "__complete echo -n mynamespace ''",
golden: "output/plugin_echo_no_directive.txt",
rels: []*release.Release{},
}, {
name: "completion for plugin bad directive",
cmd: "__complete echo ''",
golden: "output/plugin_echo_bad_directive.txt",
rels: []*release.Release{},
}}
for _, test := range tests {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
runTestCmd(t, []cmdTestCase{test})
}
}
func TestLoadPlugins_HelmNoPlugins(t *testing.T) {
settings.PluginsDirectory = "testdata/helmhome/helm/plugins"
settings.RepositoryConfig = "testdata/helmhome/helm/repository"

@ -24,6 +24,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/plugin"
)
@ -33,6 +34,7 @@ type pluginUninstallOptions struct {
func newPluginUninstallCmd(out io.Writer) *cobra.Command {
o := &pluginUninstallOptions{}
cmd := &cobra.Command{
Use: "uninstall <plugin>...",
Aliases: []string{"rm", "remove"},
@ -44,6 +46,15 @@ func newPluginUninstallCmd(out io.Writer) *cobra.Command {
return o.run(out)
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListPlugins(toComplete), completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -24,6 +24,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/plugin"
"helm.sh/helm/v3/pkg/plugin/installer"
)
@ -34,6 +35,7 @@ type pluginUpdateOptions struct {
func newPluginUpdateCmd(out io.Writer) *cobra.Command {
o := &pluginUpdateOptions{}
cmd := &cobra.Command{
Use: "update <plugin>...",
Aliases: []string{"up"},
@ -45,6 +47,15 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
return o.run(out)
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListPlugins(toComplete), completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -68,6 +69,14 @@ func newPullCmd(out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListCharts(toComplete, false)
})
f := cmd.Flags()
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it")

@ -20,7 +20,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"testing"
"helm.sh/helm/v3/pkg/repo/repotest"
@ -37,15 +36,23 @@ func TestPullCmd(t *testing.T) {
t.Fatal(err)
}
helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) <helm-testing@helm.sh>\n" +
"Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" +
"Chart Hash Verified: "
// all flags will get "-d outdir" appended.
tests := []struct {
name string
args string
existFile string
existDir string
wantError bool
wantErrorMsg string
failExpect string
expectFile string
expectDir bool
expectVerify bool
expectSha string
}{
{
name: "Basic chart fetch",
@ -74,6 +81,7 @@ func TestPullCmd(t *testing.T) {
args: "test/signtest --verify --keyring testdata/helm-test-key.pub",
expectFile: "./signtest-0.1.0.tgz",
expectVerify: true,
expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
},
{
name: "Fetch and fail verify",
@ -87,12 +95,27 @@ func TestPullCmd(t *testing.T) {
expectFile: "./signtest",
expectDir: true,
},
{
name: "Fetch untar when file with same name existed",
args: "test/test1 --untar --untardir test1",
existFile: "test1",
wantError: true,
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test1")),
},
{
name: "Fetch untar when dir with same name existed",
args: "test/test2 --untar --untardir test2",
existDir: "test2",
wantError: true,
wantErrorMsg: fmt.Sprintf("failed to untar: a file or directory with the name %s already exists", filepath.Join(srv.Root(), "test2")),
},
{
name: "Fetch, verify, untar",
args: "test/signtest --verify --keyring=testdata/helm-test-key.pub --untar --untardir signtest",
expectFile: "./signtest",
args: "test/signtest --verify --keyring=testdata/helm-test-key.pub --untar --untardir signtest2",
expectFile: "./signtest2",
expectDir: true,
expectVerify: true,
expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55",
},
{
name: "Chart fetch using repo URL",
@ -127,22 +150,38 @@ func TestPullCmd(t *testing.T) {
filepath.Join(outdir, "repositories.yaml"),
outdir,
)
// Create file or Dir before helm pull --untar, see: https://github.com/helm/helm/issues/7182
if tt.existFile != "" {
file := filepath.Join(outdir, tt.existFile)
_, err := os.Create(file)
if err != nil {
t.Fatal("err")
}
}
if tt.existDir != "" {
file := filepath.Join(outdir, tt.existDir)
err := os.Mkdir(file, 0755)
if err != nil {
t.Fatal(err)
}
}
_, out, err := executeActionCommand(cmd)
if err != nil {
if tt.wantError {
if tt.wantErrorMsg != "" && tt.wantErrorMsg == err.Error() {
t.Fatalf("Actual error %s, not equal to expected error %s", err, tt.wantErrorMsg)
}
return
}
t.Fatalf("%q reported error: %s", tt.name, err)
}
if tt.expectVerify {
pointerAddressPattern := "0[xX][A-Fa-f0-9]+"
sha256Pattern := "[A-Fa-f0-9]{64}"
verificationRegex := regexp.MustCompile(
fmt.Sprintf("Verification: &{%s sha256:%s signtest-0.1.0.tgz}\n", pointerAddressPattern, sha256Pattern))
if !verificationRegex.MatchString(out) {
t.Errorf("%q: expected match for regex %s, got %s", tt.name, verificationRegex, out)
outString := helmTestKeyOut + tt.expectSha + "\n"
if out != outString {
t.Errorf("%q: expected verification output %q, got %q", tt.name, outString, out)
}
}
ef := filepath.Join(outdir, tt.expectFile)

@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
@ -71,6 +72,14 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "Dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")

@ -43,9 +43,10 @@ type repoAddOptions struct {
password string
noUpdate bool
certFile string
keyFile string
caFile string
certFile string
keyFile string
caFile string
insecureSkipTLSverify bool
repoFile string
repoCache string
@ -75,6 +76,7 @@ func newRepoAddCmd(out io.Writer) *cobra.Command {
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.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle")
f.BoolVar(&o.insecureSkipTLSverify, "insecure-skip-tls-verify", false, "skip tls certificate checks for the repository")
return cmd
}
@ -113,13 +115,14 @@ func (o *repoAddOptions) run(out io.Writer) error {
}
c := repo.Entry{
Name: o.name,
URL: o.url,
Username: o.username,
Password: o.password,
CertFile: o.certFile,
KeyFile: o.keyFile,
CAFile: o.caFile,
Name: o.name,
URL: o.url,
Username: o.username,
Password: o.password,
CertFile: o.certFile,
KeyFile: o.keyFile,
CAFile: o.caFile,
InsecureSkipTLSverify: o.insecureSkipTLSverify,
}
r, err := repo.NewChartRepository(&c, getter.All(settings))

@ -19,13 +19,16 @@ package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"testing"
"gopkg.in/yaml.v2"
"sigs.k8s.io/yaml"
"helm.sh/helm/v3/internal/test/ensure"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/helmpath/xdg"
"helm.sh/helm/v3/pkg/repo"
"helm.sh/helm/v3/pkg/repo/repotest"
)
@ -55,7 +58,8 @@ func TestRepoAdd(t *testing.T) {
}
defer ts.Stop()
repoFile := filepath.Join(ensure.TempDir(t), "repositories.yaml")
rootDir := ensure.TempDir(t)
repoFile := filepath.Join(rootDir, "repositories.yaml")
const testRepoName = "test-name"
@ -65,6 +69,7 @@ func TestRepoAdd(t *testing.T) {
noUpdate: true,
repoFile: repoFile,
}
os.Setenv(xdg.CacheHomeEnvVar, rootDir)
if err := o.run(ioutil.Discard); err != nil {
t.Error(err)
@ -79,6 +84,15 @@ func TestRepoAdd(t *testing.T) {
t.Errorf("%s was not successfully inserted into %s", testRepoName, repoFile)
}
idx := filepath.Join(helmpath.CachePath("repository"), helmpath.CacheIndexFile(testRepoName))
if _, err := os.Stat(idx); os.IsNotExist(err) {
t.Errorf("Error cache index file was not created for repository %s", testRepoName)
}
idx = filepath.Join(helmpath.CachePath("repository"), helmpath.CacheChartsFile(testRepoName))
if _, err := os.Stat(idx); os.IsNotExist(err) {
t.Errorf("Error cache charts file was not created for repository %s", testRepoName)
}
o.noUpdate = false
if err := o.run(ioutil.Discard); err != nil {

@ -119,7 +119,7 @@ func TestRepoIndexCmd(t *testing.T) {
t.Error(err)
}
_, err = repo.LoadIndexFile(destIndex)
index, err = repo.LoadIndexFile(destIndex)
if err != nil {
t.Fatal(err)
}
@ -130,8 +130,8 @@ func TestRepoIndexCmd(t *testing.T) {
}
vs = index.Entries["compressedchart"]
if len(vs) != 3 {
t.Errorf("expected 3 versions, got %d: %#v", len(vs), vs)
if len(vs) != 1 {
t.Errorf("expected 1 versions, got %d: %#v", len(vs), vs)
}
expectedVersion = "0.3.0"

@ -18,6 +18,7 @@ package main
import (
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/pkg/errors"
@ -95,3 +96,18 @@ func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) err
// WriteJSON and WriteYAML, we shouldn't get invalid types
return nil
}
// Provide dynamic auto-completion for repo names
func compListRepos(prefix string) []string {
var rNames []string
f, err := repo.LoadFile(settings.RepositoryConfig)
if err == nil && len(f.Repositories) > 0 {
for _, repo := range f.Repositories {
if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name)
}
}
}
return rNames
}

@ -0,0 +1,25 @@
/*
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 (
"testing"
)
func TestRepoListOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "repo list")
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo"
)
@ -38,6 +39,7 @@ type repoRemoveOptions struct {
func newRepoRemoveCmd(out io.Writer) *cobra.Command {
o := &repoRemoveOptions{}
cmd := &cobra.Command{
Use: "remove [NAME]",
Aliases: []string{"rm"},
@ -51,6 +53,14 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListRepos(toComplete), completion.BashCompDirectiveNoFileComp
})
return cmd
}
@ -76,7 +86,12 @@ func (o *repoRemoveOptions) run(out io.Writer) error {
}
func removeRepoCache(root, name string) error {
idx := filepath.Join(root, helmpath.CacheIndexFile(name))
idx := filepath.Join(root, helmpath.CacheChartsFile(name))
if _, err := os.Stat(idx); err == nil {
os.Remove(idx)
}
idx = filepath.Join(root, helmpath.CacheIndexFile(name))
if _, err := os.Stat(idx); os.IsNotExist(err) {
return nil
} else if err != nil {

@ -63,10 +63,13 @@ func TestRepoRemove(t *testing.T) {
}
idx := filepath.Join(rootDir, helmpath.CacheIndexFile(testRepoName))
mf, _ := os.Create(idx)
mf.Close()
idx2 := filepath.Join(rootDir, helmpath.CacheChartsFile(testRepoName))
mf, _ = os.Create(idx2)
mf.Close()
b.Reset()
if err := rmOpts.run(b); err != nil {
@ -77,7 +80,11 @@ func TestRepoRemove(t *testing.T) {
}
if _, err := os.Stat(idx); err == nil {
t.Errorf("Error cache file was not removed for repository %s", testRepoName)
t.Errorf("Error cache index file was not removed for repository %s", testRepoName)
}
if _, err := os.Stat(idx2); err == nil {
t.Errorf("Error cache chart file was not removed for repository %s", testRepoName)
}
f, err := repo.LoadFile(repoFile)

@ -25,6 +25,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -64,6 +65,14 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")

@ -23,287 +23,12 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/tools/clientcmd"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
const (
bashCompletionFunc = `
__helm_override_flag_list=(--kubeconfig --kube-context --namespace -n)
__helm_override_flags()
{
local ${__helm_override_flag_list[*]##*-} two_word_of of var
for w in "${words[@]}"; do
if [ -n "${two_word_of}" ]; then
eval "${two_word_of##*-}=\"${two_word_of}=\${w}\""
two_word_of=
continue
fi
for of in "${__helm_override_flag_list[@]}"; do
case "${w}" in
${of}=*)
eval "${of##*-}=\"${w}\""
;;
${of})
two_word_of="${of}"
;;
esac
done
done
for var in "${__helm_override_flag_list[@]##*-}"; do
if eval "test -n \"\$${var}\""; then
eval "echo \${${var}}"
fi
done
}
__helm_override_flags_to_kubectl_flags()
{
# --kubeconfig, -n, --namespace stay the same for kubectl
# --kube-context becomes --context for kubectl
__helm_debug "${FUNCNAME[0]}: flags to convert: $1"
echo "$1" | \sed s/kube-context/context/
}
__helm_get_repos()
{
eval $(__helm_binary_name) repo list 2>/dev/null | \tail -n +2 | \cut -f1
}
__helm_get_contexts()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local template out
template="{{ range .contexts }}{{ .name }} {{ end }}"
if out=$(kubectl config -o template --template="${template}" view 2>/dev/null); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_get_namespaces()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local template out
template="{{ range .items }}{{ .metadata.name }} {{ end }}"
flags=$(__helm_override_flags_to_kubectl_flags "$(__helm_override_flags)")
__helm_debug "${FUNCNAME[0]}: override flags for kubectl are: $flags"
# Must use eval in case the flags contain a variable such as $HOME
if out=$(eval kubectl get ${flags} -o template --template=\"${template}\" namespace 2>/dev/null); then
COMPREPLY+=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_output_options()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
COMPREPLY+=( $( compgen -W "%[1]s" -- "$cur" ) )
}
__helm_binary_name()
{
local helm_binary
helm_binary="${words[0]}"
__helm_debug "${FUNCNAME[0]}: helm_binary is ${helm_binary}"
echo ${helm_binary}
}
# This function prevents the zsh shell from adding a space after
# a completion by adding a second, fake completion
__helm_zsh_comp_nospace() {
__helm_debug "${FUNCNAME[0]}: in is ${in[*]}"
local out in=("$@")
# The shell will normally add a space after these completions.
# To avoid that we should use "compopt -o nospace". However, it is not
# available in zsh.
# Instead, we trick the shell by pretending there is a second, longer match.
# We only do this if there is a single choice left for completion
# to reduce the times the user could be presented with the fake completion choice.
out=($(echo ${in[*]} | \tr " " "\n" | \grep "^${cur}"))
__helm_debug "${FUNCNAME[0]}: out is ${out[*]}"
[ ${#out[*]} -eq 1 ] && out+=("${out}.")
__helm_debug "${FUNCNAME[0]}: out is now ${out[*]}"
echo "${out[*]}"
}
# $1 = 1 if the completion should include local charts (which means file completion)
__helm_list_charts()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local repo url file out=() nospace=0 wantFiles=$1
# Handle completions for repos
for repo in $(__helm_get_repos); do
if [[ "${cur}" =~ ^${repo}/.* ]]; then
# We are doing completion from within a repo
out=$(eval $(__helm_binary_name) search repo ${cur} 2>/dev/null | \cut -f1 | \grep ^${cur})
nospace=0
elif [[ ${repo} =~ ^${cur}.* ]]; then
# We are completing a repo name
out+=(${repo}/)
nospace=1
fi
done
__helm_debug "${FUNCNAME[0]}: out after repos is ${out[*]}"
# Handle completions for url prefixes
for url in https:// http:// file://; do
if [[ "${cur}" =~ ^${url}.* ]]; then
# The user already put in the full url prefix. Return it
# back as a completion to avoid the shell doing path completion
out="${cur}"
nospace=1
elif [[ ${url} =~ ^${cur}.* ]]; then
# We are completing a url prefix
out+=(${url})
nospace=1
fi
done
__helm_debug "${FUNCNAME[0]}: out after urls is ${out[*]}"
# Handle completion for files.
# We only do this if:
# 1- There are other completions found (if there are no completions,
# the shell will do file completion itself)
# 2- If there is some input from the user (or else we will end up
# listing the entire content of the current directory which will
# be too many choices for the user to find the real repos)
if [ $wantFiles -eq 1 ] && [ -n "${out[*]}" ] && [ -n "${cur}" ]; then
for file in $(\ls); do
if [[ ${file} =~ ^${cur}.* ]]; then
# We are completing a file prefix
out+=(${file})
nospace=1
fi
done
fi
__helm_debug "${FUNCNAME[0]}: out after files is ${out[*]}"
# If the user didn't provide any input to completion,
# we provide a hint that a path can also be used
[ $wantFiles -eq 1 ] && [ -z "${cur}" ] && out+=(./ /)
__helm_debug "${FUNCNAME[0]}: out after checking empty input is ${out[*]}"
if [ $nospace -eq 1 ]; then
if [[ -n "${ZSH_VERSION}" ]]; then
# Don't let the shell add a space after the completion
local tmpout=$(__helm_zsh_comp_nospace "${out[@]}")
unset out
out=$tmpout
elif [[ $(type -t compopt) = "builtin" ]]; then
compopt -o nospace
fi
fi
__helm_debug "${FUNCNAME[0]}: final out is ${out[*]}"
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
}
__helm_list_releases()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local out filter
# Use ^ to map from the start of the release name
filter="^${words[c]}"
# Use eval in case helm_binary_name or __helm_override_flags contains a variable (e.g., $HOME/bin/h3)
if out=$(eval $(__helm_binary_name) list $(__helm_override_flags) -a -q -m 1000 -f ${filter} 2>/dev/null); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_list_repos()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local out
# Use eval in case helm_binary_name contains a variable (e.g., $HOME/bin/h3)
if out=$(__helm_get_repos); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_list_plugins()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local out
# Use eval in case helm_binary_name contains a variable (e.g., $HOME/bin/h3)
if out=$(eval $(__helm_binary_name) plugin list 2>/dev/null | \tail -n +2 | \cut -f1); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_list_charts_after_name() {
__helm_debug "${FUNCNAME[0]}: last_command is $last_command"
if [[ ${#nouns[@]} -eq 1 ]]; then
__helm_list_charts 1
fi
}
__helm_list_releases_then_charts() {
__helm_debug "${FUNCNAME[0]}: last_command is $last_command"
if [[ ${#nouns[@]} -eq 0 ]]; then
__helm_list_releases
elif [[ ${#nouns[@]} -eq 1 ]]; then
__helm_list_charts 1
fi
}
__helm_custom_func()
{
__helm_debug "${FUNCNAME[0]}: last_command is $last_command"
case ${last_command} in
helm_pull)
__helm_list_charts 0
return
;;
helm_show_*)
__helm_list_charts 1
return
;;
helm_install | helm_template)
__helm_list_charts_after_name
return
;;
helm_upgrade)
__helm_list_releases_then_charts
return
;;
helm_uninstall | helm_history | helm_status | helm_test |\
helm_rollback | helm_get_*)
__helm_list_releases
return
;;
helm_repo_remove)
__helm_list_repos
return
;;
helm_plugin_uninstall | helm_plugin_update)
__helm_list_plugins
return
;;
*)
;;
esac
}
`
)
var (
// Mapping of global flags that can have dynamic completion and the
// completion function to be used.
bashCompletionFlags = map[string]string{
"namespace": "__helm_get_namespaces",
"kube-context": "__helm_get_contexts",
}
)
var globalUsage = `The Kubernetes package manager
@ -351,13 +76,57 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
Short: "The Helm package manager for Kubernetes.",
Long: globalUsage,
SilenceUsage: true,
Args: require.NoArgs,
BashCompletionFunction: fmt.Sprintf(bashCompletionFunc, strings.Join(output.Formats(), " ")),
BashCompletionFunction: completion.GetBashCustomFunction(),
}
flags := cmd.PersistentFlags()
settings.AddFlags(flags)
// Setup shell completion for the namespace flag
flag := flags.Lookup("namespace")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if client, err := actionConfig.KubernetesClientSet(); err == nil {
// Choose a long enough timeout that the user notices somethings is not working
// but short enough that the user is not made to wait very long
to := int64(3)
completion.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to))
nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
for _, ns := range namespaces.Items {
if strings.HasPrefix(ns.Name, toComplete) {
nsNames = append(nsNames, ns.Name)
}
}
return nsNames, completion.BashCompDirectiveNoFileComp
}
}
return nil, completion.BashCompDirectiveDefault
})
// Setup shell completion for the kube-context flag
flag = flags.Lookup("kube-context")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
completion.CompDebugln("About to get the different kube-contexts")
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
if len(settings.KubeConfig) > 0 {
loadingRules = &clientcmd.ClientConfigLoadingRules{ExplicitPath: settings.KubeConfig}
}
if config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
ctxs := []string{}
for name := range config.Contexts {
if strings.HasPrefix(name, toComplete) {
ctxs = append(ctxs, name)
}
}
return ctxs, completion.BashCompDirectiveNoFileComp
}
return nil, completion.BashCompDirectiveNoFileComp
})
// We can safely ignore any errors that flags.Parse encounters since
// those errors will be caught later during the call to cmd.Execution.
// This call is required to gather configuration information prior to
@ -397,20 +166,10 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Hidden documentation generator command: 'helm docs'
newDocsCmd(out),
)
// Add annotation to flags for which we can generate completion choices
for name, completion := range bashCompletionFlags {
if cmd.Flag(name) != nil {
if cmd.Flag(name).Annotations == nil {
cmd.Flag(name).Annotations = map[string][]string{}
}
cmd.Flag(name).Annotations[cobra.BashCompCustom] = append(
cmd.Flag(name).Annotations[cobra.BashCompCustom],
completion,
)
}
}
// Setup the special hidden __complete command to allow for dynamic auto-completion
completion.NewCompleteCmd(settings, out),
)
// Add *experimental* subcommands
registryClient, err := registry.NewClient(

@ -35,23 +35,23 @@ func TestRootCmd(t *testing.T) {
}{
{
name: "defaults",
args: "home",
args: "env",
},
{
name: "with $XDG_CACHE_HOME set",
args: "home",
args: "env",
envvars: map[string]string{xdg.CacheHomeEnvVar: "/bar"},
cachePath: "/bar/helm",
},
{
name: "with $XDG_CONFIG_HOME set",
args: "home",
args: "env",
envvars: map[string]string{xdg.ConfigHomeEnvVar: "/bar"},
configPath: "/bar/helm",
},
{
name: "with $XDG_DATA_HOME set",
args: "home",
args: "env",
envvars: map[string]string{xdg.DataHomeEnvVar: "/bar"},
dataPath: "/bar/helm",
},
@ -95,3 +95,11 @@ func TestRootCmd(t *testing.T) {
})
}
}
func TestUnknownSubCmd(t *testing.T) {
_, _, err := executeActionCommand("foobar")
if err == nil || err.Error() != `unknown command "foobar" for "helm"` {
t.Errorf("Expect unknown command error, got %q", err)
}
}

@ -49,5 +49,8 @@ func TestSearchHubCmd(t *testing.T) {
t.Log(out)
t.Log(expected)
}
}
func TestSearchHubOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "search hub")
}

@ -17,8 +17,12 @@ limitations under the License.
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
@ -28,6 +32,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/search"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo"
@ -98,7 +103,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command {
func (o *searchRepoOptions) run(out io.Writer, args []string) error {
o.setupSearchedVersion()
index, err := o.buildIndex(out)
index, err := o.buildIndex()
if err != nil {
return err
}
@ -167,7 +172,7 @@ func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Res
return data, nil
}
func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
// Load the repositories.yaml
rf, err := repo.LoadFile(o.repoFile)
if isNotExist(err) || len(rf.Repositories) == 0 {
@ -180,8 +185,7 @@ func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
ind, err := repo.LoadIndexFile(f)
if err != nil {
// TODO should print to stderr
fmt.Fprintf(out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
fmt.Fprintf(os.Stderr, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
continue
}
@ -246,3 +250,137 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) e
// WriteJSON and WriteYAML, we shouldn't get invalid types
return nil
}
// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
func compListChartsOfRepo(repoName string, prefix string) []string {
var charts []string
path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))
content, err := ioutil.ReadFile(path)
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(content))
for scanner.Scan() {
fullName := fmt.Sprintf("%s/%s", repoName, scanner.Text())
if strings.HasPrefix(fullName, prefix) {
charts = append(charts, fullName)
}
}
return charts
}
if isNotExist(err) {
// If there is no cached charts file, fallback to the full index file.
// This is much slower but can happen after the caching feature is first
// installed but before the user does a 'helm repo update' to generate the
// first cached charts file.
path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
if indexFile, err := repo.LoadIndexFile(path); err == nil {
for name := range indexFile.Entries {
fullName := fmt.Sprintf("%s/%s", repoName, name)
if strings.HasPrefix(fullName, prefix) {
charts = append(charts, fullName)
}
}
return charts
}
}
return []string{}
}
// Provide dynamic auto-completion for commands that operate on charts (e.g., helm show)
// When true, the includeFiles argument indicates that completion should include local files (e.g., local charts)
func compListCharts(toComplete string, includeFiles bool) ([]string, completion.BashCompDirective) {
completion.CompDebugln(fmt.Sprintf("compListCharts with toComplete %s", toComplete))
noSpace := false
noFile := false
var completions []string
// First check completions for repos
repos := compListRepos("")
for _, repo := range repos {
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
completions = append(completions, compListChartsOfRepo(repo, toComplete)...)
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name
completions = append(completions, repoWithSlash)
noSpace = true
}
}
completion.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions))
// Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} {
if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default
// to file completion since we could be returning an empty array.
noFile = true
noSpace = true
} else if strings.HasPrefix(url, toComplete) {
// We are completing a url prefix
completions = append(completions, url)
noSpace = true
}
}
completion.CompDebugln(fmt.Sprintf("Completions after urls: %v", completions))
// Finally, provide file completion if we need to.
// We only do this if:
// 1- There are other completions found (if there are no completions,
// the shell will do file completion itself)
// 2- If there is some input from the user (or else we will end up
// listing the entire content of the current directory which will
// be too many choices for the user to find the real repos)
if includeFiles && len(completions) > 0 && len(toComplete) > 0 {
if files, err := ioutil.ReadDir("."); err == nil {
for _, file := range files {
if strings.HasPrefix(file.Name(), toComplete) {
// We are completing a file prefix
completions = append(completions, file.Name())
}
}
}
}
completion.CompDebugln(fmt.Sprintf("Completions after files: %v", completions))
// If the user didn't provide any input to completion,
// we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 {
completions = append(completions, "./", "/")
}
completion.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions))
directive := completion.BashCompDirectiveDefault
if noFile {
directive = directive | completion.BashCompDirectiveNoFileComp
}
if noSpace {
directive = directive | completion.BashCompDirectiveNoSpace
// The completion.BashCompDirective flags do not work for zsh right now.
// We handle it ourselves instead.
completions = compEnforceNoSpace(completions)
}
return completions, directive
}
// This function prevents the shell from adding a space after
// a completion by adding a second, fake completion.
// It is only needed for zsh, but we cannot tell which shell
// is being used here, so we do the fake completion all the time;
// there are no real downsides to doing this for bash as well.
func compEnforceNoSpace(completions []string) []string {
// To prevent the shell from adding space after the completion,
// we trick it by pretending there is a second, longer match.
// We only do this if there is a single choice for completion.
if len(completions) == 1 {
completions = append(completions, completions[0]+".")
completion.CompDebugln(fmt.Sprintf("compEnforceNoSpace: completions now are %v", completions))
}
return completions
}

@ -83,3 +83,7 @@ func TestSearchRepositoriesCmd(t *testing.T) {
}
runTestCmd(t, tests)
}
func TestSearchRepoOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "search repo")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -61,6 +62,14 @@ func newShowCmd(out io.Writer) *cobra.Command {
Args: require.NoArgs,
}
// Function providing dynamic auto-completion
validArgsFunc := func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListCharts(toComplete, true)
}
all := &cobra.Command{
Use: "all [CHART]",
Short: "shows all information of the chart",
@ -145,6 +154,9 @@ func newShowCmd(out io.Writer) *cobra.Command {
for _, subCmd := range cmds {
addChartPathOptionsFlags(subCmd.Flags(), &client.ChartPathOptions)
showCommand.AddCommand(subCmd)
// Register the completion function for each subcommand
completion.RegisterValidArgsFunc(subCmd, validArgsFunc)
}
return showCommand

@ -25,6 +25,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/output"
@ -65,8 +66,25 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.PersistentFlags()
f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
bindOutputFlag(cmd, &outfmt)
return cmd

@ -108,3 +108,66 @@ func mustParseTime(t string) helmtime.Time {
res, _ := helmtime.Parse(time.RFC3339, t)
return res
}
func TestStatusCompletion(t *testing.T) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "athos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "porthos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "aramis",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "dartagnan",
Namespace: "gascony",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}}
}
tests := []cmdTestCase{{
name: "completion for status",
cmd: "__complete status a",
golden: "output/status-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for status with too many arguments",
cmd: "__complete status dartagnan ''",
golden: "output/status-wrong-args-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for status with too many arguments",
cmd: "__complete status --debug a",
golden: "output/status-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}}
runTestCmd(t, tests)
}
func TestStatusRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "status")
}
func TestStatusOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "status")
}

@ -24,14 +24,14 @@ import (
"regexp"
"strings"
"helm.sh/helm/v3/pkg/releaseutil"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/values"
"helm.sh/helm/v3/pkg/releaseutil"
)
const templateDesc = `
@ -52,7 +52,7 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
cmd := &cobra.Command{
Use: "template [NAME] [CHART]",
Short: fmt.Sprintf("locally render templates"),
Short: "locally render templates",
Long: templateDesc,
Args: require.MinimumNArgs(1),
RunE: func(_ *cobra.Command, args []string) error {
@ -61,68 +61,75 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
client.Replace = true // Skip the name check
client.ClientOnly = !validate
client.APIVersions = chartutil.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out)
if err != nil {
return err
}
var manifests bytes.Buffer
if includeCrds {
for _, f := range rel.Chart.CRDs() {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", f.Name, f.Data)
if err != nil && !settings.Debug {
if rel != nil {
return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", err)
}
return err
}
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
// We ignore a potential error here because, when the --debug flag was specified,
// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
for _, m := range rel.Hooks {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
if !client.DisableHooks {
for _, m := range rel.Hooks {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
}
}
}
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
for _, manifest := range splitManifests {
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
for _, manifest := range splitManifests {
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
manifestPath := filepath.Join(manifestPathSplit...)
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if f == manifestPath {
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
manifestPath := filepath.Join(manifestPathSplit...)
// if the filepath provided matches a manifest path in the
// chart, render that manifest
if f == manifestPath {
manifestsToRender = append(manifestsToRender, manifest)
missing = false
if missing {
return fmt.Errorf("could not find template %s in chart", f)
}
}
if missing {
return fmt.Errorf("could not find template %s in chart", f)
}
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
return nil
return err
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
return compInstall(args, toComplete, client)
})
f := cmd.Flags()
addInstallFlags(f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
@ -131,6 +138,8 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
bindPostRenderFlag(cmd, &client.PostRenderer)
return cmd
}

@ -84,6 +84,36 @@ func TestTemplateCmd(t *testing.T) {
cmd: fmt.Sprintf("template '%s' --include-crds", chartPath),
golden: "output/template-with-crds.txt",
},
{
name: "template with show-only one",
cmd: fmt.Sprintf("template '%s' --show-only templates/service.yaml", chartPath),
golden: "output/template-show-only-one.txt",
},
{
name: "template with show-only multiple",
cmd: fmt.Sprintf("template '%s' --show-only templates/service.yaml --show-only charts/subcharta/templates/service.yaml", chartPath),
golden: "output/template-show-only-multiple.txt",
},
{
name: "sorted output of manifests (order of filenames, then order of objects within each YAML file)",
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/object-order"),
golden: "output/object-order.txt",
// Helm previously used random file order. Repeat the test so we
// don't accidentally get the expected result.
repeat: 10,
},
{
name: "chart with template with invalid yaml",
cmd: fmt.Sprintf("template '%s'", "testdata/testcharts/chart-with-template-with-invalid-yaml"),
wantError: true,
golden: "output/template-with-invalid-yaml.txt",
},
{
name: "chart with template with invalid yaml (--debug)",
cmd: fmt.Sprintf("template '%s' --debug", "testdata/testcharts/chart-with-template-with-invalid-yaml"),
wantError: true,
golden: "output/template-with-invalid-yaml-debug.txt",
},
}
runTestCmd(t, tests)
}

@ -0,0 +1,13 @@
#!/usr/bin/env sh
echo "plugin.complete was called"
echo "Namespace: ${HELM_NAMESPACE:-NO_NS}"
echo "Num args received: ${#}"
echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then
echo ":4"
else
echo ":2"
fi

@ -0,0 +1,14 @@
#!/usr/bin/env sh
echo "echo plugin.complete was called"
echo "Namespace: ${HELM_NAMESPACE:-NO_NS}"
echo "Num args received: ${#}"
echo "Args received: ${@}"
# Final printout is the optional completion directive of the form :<directive>
if [ "$HELM_NAMESPACE" = "default" ]; then
# Output an invalid directive, which should be ignored
echo ":2222"
# else
# Don't include the directive, to test it is really optional
fi

@ -0,0 +1,13 @@
name: env
commands:
- name: list
flags:
- a
- all
- log
- name: remove
validArgs:
- all
- one
flags:
- global

@ -0,0 +1,5 @@
commands:
- name: code
flags:
- a
- b

@ -0,0 +1,19 @@
name: wrongname
commands:
- name: empty
- name: full
commands:
- name: more
validArgs:
- one
- two
flags:
- b
- ball
- name: less
flags:
- a
- all
flags:
- z
- q

@ -0,0 +1,16 @@
==> Linting testdata/testcharts/chart-with-bad-subcharts
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/: directory not found
==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/bad-subchart
[ERROR] Chart.yaml: name is required
[ERROR] Chart.yaml: apiVersion is required. The value must be either "v1" or "v2"
[ERROR] Chart.yaml: version is required
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/: directory not found
==> Linting testdata/testcharts/chart-with-bad-subcharts/charts/good-subchart
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/: directory not found
Error: 3 chart(s) linted, 1 chart(s) failed

@ -0,0 +1,5 @@
==> Linting testdata/testcharts/chart-with-bad-subcharts
[INFO] Chart.yaml: icon is recommended
[WARNING] templates/: directory not found
1 chart(s) linted, 0 chart(s) failed

@ -0,0 +1,2 @@
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
starlord milano 2 2016-01-16 00:00:01 +0000 UTC deployed chickadee-1.0.0 0.0.1

@ -0,0 +1,191 @@
---
# Source: object-order/templates/01-a.yml
# 1
kind: NetworkPolicy
apiVersion: networking.k8s.io/v1
metadata:
name: first
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/01-a.yml
# 2
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: second
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/01-a.yml
# 3
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: third
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 5
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: fifth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 7
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: seventh
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 8
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: eighth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 9
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ninth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 10
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tenth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 11
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: eleventh
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 12
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: twelfth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 13
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: thirteenth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 14
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: fourteenth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/02-b.yml
# 15 (11th object within 02-b.yml, in order to test `SplitManifests` which assigns `manifest-10`
# to this object which should then come *after* `manifest-9`)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: fifteenth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress
---
# Source: object-order/templates/01-a.yml
# 4 (Deployment should come after all NetworkPolicy manifests, since 'helm template' outputs in install order)
apiVersion: apps/v1
kind: Deployment
metadata:
name: fourth
spec:
selector:
matchLabels:
pod: fourth
replicas: 1
template:
metadata:
labels:
pod: fourth
spec:
containers:
- name: hello-world
image: gcr.io/google-samples/node-hello:1.0
---
# Source: object-order/templates/02-b.yml
# 6 (implementation detail: currently, 'helm template' outputs hook manifests last; and yes, NetworkPolicy won't make a reasonable hook, this is just a dummy unit test manifest)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
annotations:
"helm.sh/hook": pre-install
name: sixth
spec:
podSelector: {}
policyTypes:
- Egress
- Ingress

@ -0,0 +1,4 @@
table
json
yaml
:0

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:4

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: default
Num args received: 2
Args received: --myflag
:4

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: mynamespace
Num args received: 2
Args received: --myflag start
:2

@ -0,0 +1,5 @@
plugin.complete was called
Namespace: mynamespace
Num args received: 1
Args received:
:2

@ -0,0 +1,5 @@
echo plugin.complete was called
Namespace: default
Num args received: 1
Args received:
:0

@ -0,0 +1,5 @@
echo plugin.complete was called
Namespace: mynamespace
Num args received: 1
Args received:
:0

@ -0,0 +1,3 @@
aramis
athos
:4

@ -0,0 +1,39 @@
---
# Source: subchart1/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchart1
labels:
helm.sh/chart: "subchart1-0.1.0"
app.kubernetes.io/instance: "RELEASE-NAME"
kube-version/major: "1"
kube-version/minor: "16"
kube-version/version: "v1.16.0"
kube-api-version/test: v1
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchart1
---
# Source: subchart1/charts/subcharta/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subcharta
labels:
helm.sh/chart: "subcharta-0.1.0"
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: apache
selector:
app.kubernetes.io/name: subcharta

@ -0,0 +1,22 @@
---
# Source: subchart1/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: subchart1
labels:
helm.sh/chart: "subchart1-0.1.0"
app.kubernetes.io/instance: "RELEASE-NAME"
kube-version/major: "1"
kube-version/minor: "16"
kube-version/version: "v1.16.0"
kube-api-version/test: v1
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
name: nginx
selector:
app.kubernetes.io/name: subchart1

@ -0,0 +1,13 @@
---
# Source: chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: "RELEASE-NAME-my-alpine"
spec:
containers:
- name: waiter
image: "alpine:3.9"
command: ["/bin/sleep","9000"]
invalid
Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':'

@ -0,0 +1,3 @@
Error: YAML parse error on chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml: error converting YAML to JSON: yaml: line 11: could not find expected ':'
Use --debug flag to render out invalid YAML

@ -1 +1 @@
version.BuildInfo{Version:"v3.0", GitCommit:"", GitTreeState:"", GoVersion:""}
version.BuildInfo{Version:"v3.1", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1 +1 @@
version.BuildInfo{Version:"v3.0", GitCommit:"", GitTreeState:"", GoVersion:""}
version.BuildInfo{Version:"v3.1", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1 +1 @@
Version: v3.0
Version: v3.1

@ -1 +1 @@
version.BuildInfo{Version:"v3.0", GitCommit:"", GitTreeState:"", GoVersion:""}
version.BuildInfo{Version:"v3.1", GitCommit:"", GitTreeState:"", GoVersion:""}

@ -1,4 +1,4 @@
#Alpine: A simple Helm chart
# Alpine: A simple Helm chart
Run a single pod of Alpine Linux.

@ -14,7 +14,7 @@ metadata:
app.kubernetes.io/version: {{ .Chart.AppVersion }}
# This makes it easy to audit chart usage.
helm.sh/chart: "{{.Chart.Name}}-{{.Chart.Version}}"
values: {{.Values.test.Name}}
values: {{.Values.Name}}
spec:
# This shows how to use a simple value. This will look for a passed-in value
# called restartPolicy. If it is not found, it will use the default value.

@ -0,0 +1,4 @@
apiVersion: v1
description: Chart with bad subcharts
name: chart-with-bad-subcharts
version: 0.0.1

@ -0,0 +1,4 @@
apiVersion: v1
description: Good subchart
name: good-subchart
version: 0.0.1

@ -0,0 +1,5 @@
dependencies:
- name: good-subchart
version: 0.0.1
- name: bad-subchart
version: 0.0.1

@ -0,0 +1,8 @@
apiVersion: v1
description: Deploy a basic Alpine Linux pod
home: https://helm.sh/helm
name: chart-with-template-with-invalid-yaml
sources:
- https://github.com/helm/helm
version: 0.1.0
type: application

@ -0,0 +1,13 @@
#Alpine: A simple Helm chart
Run a single pod of Alpine Linux.
This example was generated using the command `helm create alpine`.
The `templates/` directory contains a very simple pod resource with a
couple of parameters.
The `values.yaml` file contains the default values for the
`alpine-pod.yaml` template.
You can install this example using `helm install ./alpine`.

@ -0,0 +1,10 @@
apiVersion: v1
kind: Pod
metadata:
name: "{{.Release.Name}}-{{.Values.Name}}"
spec:
containers:
- name: waiter
image: "alpine:3.9"
command: ["/bin/sleep","9000"]
invalid

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save