diff --git a/.golangci.yml b/.golangci.yml index 2c3b6234d..491e648a1 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -17,6 +17,7 @@ linters: - structcheck - unused - varcheck + - staticcheck linters-settings: gofmt: diff --git a/ADOPTERS.md b/ADOPTERS.md index 5dccb8fc6..46b42b8a0 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -1,12 +1,15 @@ - To add your organization to this list, simply add your organization's name, - optionally with a link. The list is in alphabetical order. + 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) -- [Microsoft](https://microsoft.com) - [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) +- [Ville de Montreal](https://montreal.ca) + _This file is part of the CNCF official documentation for projects._ diff --git a/Makefile b/Makefile index eea3b1e9c..446fbc0ed 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ BINDIR := $(CURDIR)/bin DIST_DIRS := find * -type d -exec 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 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 linux-s390x.tar.gz linux-s390x.tar.gz.sha256 windows-amd64.zip windows-amd64.zip.sha256 +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) @@ -24,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) @@ -181,6 +181,21 @@ checksum: clean: @rm -rf $(BINDIR) ./_dist +.PHONY: release-notes +release-notes: + @if [ ! -d "./_dist" ]; then \ + echo "please run 'make fetch-release' first" && \ + exit 1; \ + fi + @if [ -z "${PREVIOUS_RELEASE}" ]; then \ + echo "please set PREVIOUS_RELEASE environment variable" \ + && exit 1; \ + fi + + @./scripts/release-notes.sh ${PREVIOUS_RELEASE} ${VERSION} + + + .PHONY: info info: @echo "Version: ${VERSION}" diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go index 112d5123f..257387547 100644 --- a/cmd/helm/helm.go +++ b/cmd/helm/helm.go @@ -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 @@ -67,9 +73,23 @@ func main() { actionConfig := new(action.Configuration) cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:]) - if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), os.Getenv("HELM_DRIVER"), debug); err != nil { + 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 { debug("%+v", err) @@ -95,3 +115,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()) +} diff --git a/cmd/helm/helm_test.go b/cmd/helm/helm_test.go index 8c6c492f8..94646a5a3 100644 --- a/cmd/helm/helm_test.go +++ b/cmd/helm/helm_test.go @@ -18,7 +18,6 @@ package main import ( "bytes" - "fmt" "io/ioutil" "os" "os/exec" @@ -97,39 +96,11 @@ func storageFixture() *storage.Storage { return storage.Init(driver.NewMemory()) } -// go-shellwords does not handle empty arguments properly -// https://github.com/mattn/go-shellwords/issues/5#issuecomment-573431458 -// -// This method checks if the last argument was an empty one, -// and if go-shellwords missed it, we add it ourselves. -// -// This is important for completion tests as completion often -// uses an empty last parameter. -func checkLastEmpty(in string, out []string) []string { - lastIndex := len(in) - 1 - - if lastIndex >= 1 && (in[lastIndex] == '"' && in[lastIndex-1] == '"' || - in[lastIndex] == '\'' && in[lastIndex-1] == '\'') { - // The last parameter of 'in' was empty ("" or ''), let's make sure it was detected. - if len(out) > 0 && out[len(out)-1] != "" { - // Bug from go-shellwords: - // 'out' does not have the empty parameter. We add it ourselves as a workaround. - out = append(out, "") - } else { - fmt.Println("WARNING: go-shellwords seems to have been fixed. This workaround can be removed.") - } - } - return out -} - func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) { args, err := shellwords.Parse(cmd) if err != nil { return nil, "", err } - // Workaround the bug in shellwords - args = checkLastEmpty(cmd, args) - buf := new(bytes.Buffer) actionConfig := &action.Configuration{ diff --git a/cmd/helm/install.go b/cmd/helm/install.go index ec2c75a12..719dc9014 100644 --- a/cmd/helm/install.go +++ b/cmd/helm/install.go @@ -136,6 +136,7 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } func addInstallFlags(f *pflag.FlagSet, client *action.Install, valueOpts *values.Options) { + f.BoolVar(&client.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") diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go index bc0d1852b..fe39a5741 100644 --- a/cmd/helm/lint.go +++ b/cmd/helm/lint.go @@ -106,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 { diff --git a/cmd/helm/load_plugins.go b/cmd/helm/load_plugins.go index f2fb5c01d..e56feab40 100644 --- a/cmd/helm/load_plugins.go +++ b/cmd/helm/load_plugins.go @@ -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 :, 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 +} diff --git a/cmd/helm/plugin_test.go b/cmd/helm/plugin_test.go index 3fd3a4197..e43f277a5 100644 --- a/cmd/helm/plugin_test.go +++ b/cmd/helm/plugin_test.go @@ -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" diff --git a/cmd/helm/pull_test.go b/cmd/helm/pull_test.go index 1aca66100..d4661f928 100644 --- a/cmd/helm/pull_test.go +++ b/cmd/helm/pull_test.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "testing" "helm.sh/helm/v3/pkg/repo/repotest" @@ -37,6 +36,10 @@ func TestPullCmd(t *testing.T) { t.Fatal(err) } + helmTestKeyOut := "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \n" + + "Using Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\n" + + "Chart Hash Verified: " + // all flags will get "-d outdir" appended. tests := []struct { name string @@ -49,6 +52,7 @@ func TestPullCmd(t *testing.T) { expectFile string expectDir bool expectVerify bool + expectSha string }{ { name: "Basic chart fetch", @@ -77,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", @@ -110,6 +115,7 @@ func TestPullCmd(t *testing.T) { expectFile: "./signtest2", expectDir: true, expectVerify: true, + expectSha: "sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55", }, { name: "Chart fetch using repo URL", @@ -171,13 +177,11 @@ func TestPullCmd(t *testing.T) { } 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) diff --git a/cmd/helm/repo_add.go b/cmd/helm/repo_add.go index e6afce3d5..3d36fd0ed 100644 --- a/cmd/helm/repo_add.go +++ b/cmd/helm/repo_add.go @@ -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)) diff --git a/cmd/helm/root.go b/cmd/helm/root.go index 6ce1dcbf4..3ebea3bae 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -24,37 +24,13 @@ import ( "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/tools/clientcmd" - "helm.sh/helm/v3/cmd/helm/require" "helm.sh/helm/v3/internal/completion" "helm.sh/helm/v3/internal/experimental/registry" "helm.sh/helm/v3/pkg/action" ) -const ( - contextCompFunc = ` -__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 -} -` -) - -var ( - // Mapping of global flags that can have dynamic completion and the - // completion function to be used. - bashCompletionFlags = map[string]string{ - // Cannot convert the kube-context flag to Go completion yet because - // an incomplete kube-context will make actionConfig.Init() fail at the very start - "kube-context": "__helm_get_contexts", - } -) - var globalUsage = `The Kubernetes package manager Common actions for Helm: @@ -100,15 +76,14 @@ 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("%s%s", contextCompFunc, completion.GetBashCustomFunction()), + BashCompletionFunction: completion.GetBashCustomFunction(), } flags := cmd.PersistentFlags() settings.AddFlags(flags) - flag := flags.Lookup("namespace") // 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 @@ -129,6 +104,29 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string 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 @@ -173,19 +171,6 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string completion.NewCompleteCmd(settings, 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, - ) - } - } - // Add *experimental* subcommands registryClient, err := registry.NewClient( registry.ClientOptDebug(settings.Debug), diff --git a/cmd/helm/root_test.go b/cmd/helm/root_test.go index df592a96d..e1fa1fc27 100644 --- a/cmd/helm/root_test.go +++ b/cmd/helm/root_test.go @@ -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) + } +} diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go index 9f5af1e3c..8a8ac379d 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -22,6 +22,7 @@ import ( "fmt" "io" "io/ioutil" + "os" "path/filepath" "strings" @@ -102,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 } @@ -171,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 { @@ -184,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 } diff --git a/cmd/helm/show.go b/cmd/helm/show.go index a82ad2777..ac38ed5af 100644 --- a/cmd/helm/show.go +++ b/cmd/helm/show.go @@ -77,11 +77,7 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowAll - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) - if err != nil { - return err - } - output, err := client.Run(cp) + output, err := runShow(args, client) if err != nil { return err } @@ -97,11 +93,7 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowValues - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) - if err != nil { - return err - } - output, err := client.Run(cp) + output, err := runShow(args, client) if err != nil { return err } @@ -117,11 +109,7 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowChart - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) - if err != nil { - return err - } - output, err := client.Run(cp) + output, err := runShow(args, client) if err != nil { return err } @@ -137,11 +125,7 @@ func newShowCmd(out io.Writer) *cobra.Command { Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client.OutputFormat = action.ShowReadme - cp, err := client.ChartPathOptions.LocateChart(args[0], settings) - if err != nil { - return err - } - output, err := client.Run(cp) + output, err := runShow(args, client) if err != nil { return err } @@ -152,8 +136,7 @@ func newShowCmd(out io.Writer) *cobra.Command { cmds := []*cobra.Command{all, readmeSubCmd, valuesSubCmd, chartSubCmd} for _, subCmd := range cmds { - addChartPathOptionsFlags(subCmd.Flags(), &client.ChartPathOptions) - showCommand.AddCommand(subCmd) + addShowFlags(showCommand, subCmd, client) // Register the completion function for each subcommand completion.RegisterValidArgsFunc(subCmd, validArgsFunc) @@ -161,3 +144,25 @@ func newShowCmd(out io.Writer) *cobra.Command { return showCommand } + +func addShowFlags(showCmd *cobra.Command, subCmd *cobra.Command, client *action.Show) { + f := subCmd.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") + addChartPathOptionsFlags(f, &client.ChartPathOptions) + showCmd.AddCommand(subCmd) +} + +func runShow(args []string, client *action.Show) (string, error) { + debug("Original chart version: %q", client.Version) + if client.Version == "" && client.Devel { + debug("setting version to >0.0.0-0") + client.Version = ">0.0.0-0" + } + + cp, err := client.ChartPathOptions.LocateChart(args[0], settings) + if err != nil { + return "", err + } + return client.Run(cp) +} diff --git a/cmd/helm/show_test.go b/cmd/helm/show_test.go new file mode 100644 index 000000000..00d7c8145 --- /dev/null +++ b/cmd/helm/show_test.go @@ -0,0 +1,82 @@ +/* +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" + "path/filepath" + "strings" + "testing" + + "helm.sh/helm/v3/pkg/repo/repotest" +) + +func TestShowPreReleaseChart(t *testing.T) { + srv, err := repotest.NewTempServer("testdata/testcharts/*.tgz*") + if err != nil { + t.Fatal(err) + } + defer srv.Stop() + + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + args string + flags string + fail bool + expectedErr string + }{ + { + name: "show pre-release chart", + args: "test/pre-release-chart", + fail: true, + expectedErr: "failed to download \"test/pre-release-chart\"", + }, + { + name: "show pre-release chart with 'devel' flag", + args: "test/pre-release-chart", + flags: "--devel", + fail: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + outdir := srv.Root() + cmd := fmt.Sprintf("show all '%s' %s --repository-config %s --repository-cache %s", + tt.args, + tt.flags, + filepath.Join(outdir, "repositories.yaml"), + outdir, + ) + //_, out, err := executeActionCommand(cmd) + _, _, err := executeActionCommand(cmd) + if err != nil { + if tt.fail { + if !strings.Contains(err.Error(), tt.expectedErr) { + t.Errorf("%q expected error: %s, got: %s", tt.name, tt.expectedErr, err.Error()) + } + return + } + t.Errorf("%q reported error: %s", tt.name, err) + } + }) + } +} diff --git a/cmd/helm/template.go b/cmd/helm/template.go index 320718344..bd14cde1d 100644 --- a/cmd/helm/template.go +++ b/cmd/helm/template.go @@ -24,8 +24,6 @@ import ( "regexp" "strings" - "helm.sh/helm/v3/pkg/releaseutil" - "github.com/spf13/cobra" "helm.sh/helm/v3/cmd/helm/require" @@ -33,6 +31,7 @@ import ( "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 = ` @@ -53,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 { @@ -64,57 +63,65 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client.APIVersions = chartutil.VersionSet(extraAPIs) client.IncludeCRDs = includeCrds rel, err := runInstall(args, client, valueOpts, out) - if err != nil { + + if err != nil && !settings.Debug { + if rel != nil { + return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", err) + } return err } - var manifests bytes.Buffer - 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()) } - for _, m := range manifestsToRender { - fmt.Fprintf(out, "---\n%s\n", m) - } - } else { - fmt.Fprintf(out, "%s", manifests.String()) } - return nil + return err }, } diff --git a/cmd/helm/template_test.go b/cmd/helm/template_test.go index dc7987d01..3fd139fad 100644 --- a/cmd/helm/template_test.go +++ b/cmd/helm/template_test.go @@ -102,6 +102,18 @@ func TestTemplateCmd(t *testing.T) { // 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) } diff --git a/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete new file mode 100755 index 000000000..2b00c2281 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/args/plugin.complete @@ -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 : +if [ "$HELM_NAMESPACE" = "default" ]; then + echo ":4" +else + echo ":2" +fi diff --git a/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/echo/completion.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete new file mode 100755 index 000000000..6bc73d130 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/echo/plugin.complete @@ -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 : +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 diff --git a/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml new file mode 100644 index 000000000..e479a0503 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/env/completion.yaml @@ -0,0 +1,13 @@ +name: env +commands: + - name: list + flags: + - a + - all + - log + - name: remove + validArgs: + - all + - one +flags: +- global diff --git a/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml new file mode 100644 index 000000000..e5bf440f6 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/exitwith/completion.yaml @@ -0,0 +1,5 @@ +commands: + - name: code + flags: + - a + - b diff --git a/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml new file mode 100644 index 000000000..e0b161c69 --- /dev/null +++ b/cmd/helm/testdata/helmhome/helm/plugins/fullenv/completion.yaml @@ -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 diff --git a/cmd/helm/testdata/output/plugin_args_comp.txt b/cmd/helm/testdata/output/plugin_args_comp.txt new file mode 100644 index 000000000..8fb01cc23 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_comp.txt @@ -0,0 +1,5 @@ +plugin.complete was called +Namespace: default +Num args received: 1 +Args received: +:4 diff --git a/cmd/helm/testdata/output/plugin_args_flag_comp.txt b/cmd/helm/testdata/output/plugin_args_flag_comp.txt new file mode 100644 index 000000000..92f0e58a8 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_flag_comp.txt @@ -0,0 +1,5 @@ +plugin.complete was called +Namespace: default +Num args received: 2 +Args received: --myflag +:4 diff --git a/cmd/helm/testdata/output/plugin_args_many_args_comp.txt b/cmd/helm/testdata/output/plugin_args_many_args_comp.txt new file mode 100644 index 000000000..86fa768bb --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_many_args_comp.txt @@ -0,0 +1,5 @@ +plugin.complete was called +Namespace: mynamespace +Num args received: 2 +Args received: --myflag start +:2 diff --git a/cmd/helm/testdata/output/plugin_args_ns_comp.txt b/cmd/helm/testdata/output/plugin_args_ns_comp.txt new file mode 100644 index 000000000..e12867daa --- /dev/null +++ b/cmd/helm/testdata/output/plugin_args_ns_comp.txt @@ -0,0 +1,5 @@ +plugin.complete was called +Namespace: mynamespace +Num args received: 1 +Args received: +:2 diff --git a/cmd/helm/testdata/output/plugin_echo_bad_directive.txt b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt new file mode 100644 index 000000000..f4b86cd47 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_echo_bad_directive.txt @@ -0,0 +1,5 @@ +echo plugin.complete was called +Namespace: default +Num args received: 1 +Args received: +:0 diff --git a/cmd/helm/testdata/output/plugin_echo_no_directive.txt b/cmd/helm/testdata/output/plugin_echo_no_directive.txt new file mode 100644 index 000000000..6266dd4d9 --- /dev/null +++ b/cmd/helm/testdata/output/plugin_echo_no_directive.txt @@ -0,0 +1,5 @@ +echo plugin.complete was called +Namespace: mynamespace +Num args received: 1 +Args received: +:0 diff --git a/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt b/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt new file mode 100644 index 000000000..c1f51185c --- /dev/null +++ b/cmd/helm/testdata/output/template-with-invalid-yaml-debug.txt @@ -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 ':' diff --git a/cmd/helm/testdata/output/template-with-invalid-yaml.txt b/cmd/helm/testdata/output/template-with-invalid-yaml.txt new file mode 100644 index 000000000..687227b90 --- /dev/null +++ b/cmd/helm/testdata/output/template-with-invalid-yaml.txt @@ -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 diff --git a/cmd/helm/testdata/testcharts/alpine/README.md b/cmd/helm/testdata/testcharts/alpine/README.md index fcf7ee017..05d39dbbc 100644 --- a/cmd/helm/testdata/testcharts/alpine/README.md +++ b/cmd/helm/testdata/testcharts/alpine/README.md @@ -1,4 +1,4 @@ -#Alpine: A simple Helm chart +# Alpine: A simple Helm chart Run a single pod of Alpine Linux. diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml new file mode 100644 index 000000000..29b477b06 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/Chart.yaml @@ -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 diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md new file mode 100644 index 000000000..fcf7ee017 --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/README.md @@ -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`. diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml new file mode 100644 index 000000000..697cb50fe --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/templates/alpine-pod.yaml @@ -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 diff --git a/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml new file mode 100644 index 000000000..807e12aea --- /dev/null +++ b/cmd/helm/testdata/testcharts/chart-with-template-with-invalid-yaml/values.yaml @@ -0,0 +1 @@ +Name: my-alpine diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go index 54badb32c..af8ff68e3 100644 --- a/cmd/helm/upgrade.go +++ b/cmd/helm/upgrade.go @@ -65,6 +65,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { client := action.NewUpgrade(cfg) valueOpts := &values.Options{} var outfmt output.Format + var createNamespace bool cmd := &cobra.Command{ Use: "upgrade [RELEASE] [CHART]", @@ -90,8 +91,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { } if client.Install { - // If a release does not exist, install it. If another error occurs during - // the check, ignore the error and continue with the upgrade. + // If a release does not exist, install it. histClient := action.NewHistory(cfg) histClient.Max = 1 if _, err := histClient.Run(args[0]); err == driver.ErrReleaseNotFound { @@ -100,21 +100,26 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { fmt.Fprintf(out, "Release %q does not exist. Installing it now.\n", args[0]) } instClient := action.NewInstall(cfg) + instClient.CreateNamespace = createNamespace instClient.ChartPathOptions = client.ChartPathOptions instClient.DryRun = client.DryRun instClient.DisableHooks = client.DisableHooks + instClient.SkipCRDs = client.SkipCRDs instClient.Timeout = client.Timeout instClient.Wait = client.Wait instClient.Devel = client.Devel instClient.Namespace = client.Namespace instClient.Atomic = client.Atomic instClient.PostRenderer = client.PostRenderer + instClient.DisableOpenAPIValidation = client.DisableOpenAPIValidation rel, err := runInstall(args, instClient, valueOpts, out) if err != nil { return err } return outfmt.Write(out, &statusPrinter{rel, settings.Debug}) + } else if err != nil { + return err } } @@ -158,6 +163,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { }) f := cmd.Flags() + f.BoolVar(&createNamespace, "create-namespace", false, "if --install is set, create the release namespace if not present") f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install") 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.DryRun, "dry-run", false, "simulate an upgrade") @@ -165,6 +171,8 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { f.MarkDeprecated("recreate-pods", "functionality will no longer be updated. Consult the documentation for other methods to recreate pods") f.BoolVar(&client.Force, "force", false, "force resource updates through a replacement strategy") f.BoolVar(&client.DisableHooks, "no-hooks", false, "disable pre/post upgrade hooks") + f.BoolVar(&client.DisableOpenAPIValidation, "disable-openapi-validation", false, "if set, the upgrade process will not validate rendered templates against the Kubernetes OpenAPI Schema") + f.BoolVar(&client.SkipCRDs, "skip-crds", false, "if set, no CRDs will be installed when an upgrade is performed with install flag enabled. By default, CRDs are installed if not already present, when an upgrade is performed with install flag enabled") f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)") f.BoolVar(&client.ResetValues, "reset-values", false, "when upgrading, reset the values to the ones built into the chart") f.BoolVar(&client.ReuseValues, "reuse-values", false, "when upgrading, reuse the last release's values and merge in any overrides from the command line via --set and -f. If '--reset-values' is specified, this is ignored") diff --git a/cmd/helm/verify.go b/cmd/helm/verify.go index d3ae517c9..f26fb377f 100644 --- a/cmd/helm/verify.go +++ b/cmd/helm/verify.go @@ -16,6 +16,7 @@ limitations under the License. package main import ( + "fmt" "io" "github.com/spf13/cobra" @@ -44,7 +45,14 @@ func newVerifyCmd(out io.Writer) *cobra.Command { Long: verifyDesc, Args: require.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return client.Run(args[0]) + err := client.Run(args[0]) + if err != nil { + return err + } + + fmt.Fprint(out, client.Out) + + return nil }, } diff --git a/cmd/helm/verify_test.go b/cmd/helm/verify_test.go index a70051ff6..ccbcb3cf2 100644 --- a/cmd/helm/verify_test.go +++ b/cmd/helm/verify_test.go @@ -65,7 +65,7 @@ func TestVerifyCmd(t *testing.T) { { name: "verify validates a properly signed chart", cmd: "verify testdata/testcharts/signtest-0.1.0.tgz --keyring testdata/helm-test-key.pub", - expect: "", + expect: "Signed by: Helm Testing (This key should only be used for testing. DO NOT TRUST.) \nUsing Key With Fingerprint: 5E615389B53CA37F0EE60BD3843BBF981FC18762\nChart Hash Verified: sha256:e5ef611620fb97704d8751c16bab17fedb68883bfb0edc76f78a70e9173f9b55\n", wantError: false, }, } diff --git a/go.mod b/go.mod index 696c2b6d2..7ba7a5542 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/gobwas/glob v0.2.3 github.com/gofrs/flock v0.7.1 github.com/gosuri/uitable v0.0.4 - github.com/mattn/go-shellwords v1.0.9 + github.com/mattn/go-shellwords v1.0.10 github.com/mitchellh/copystructure v1.0.0 github.com/opencontainers/go-digest v1.0.0-rc1 github.com/opencontainers/image-spec v1.0.1 @@ -29,13 +29,13 @@ require ( github.com/stretchr/testify v1.4.0 github.com/xeipuuv/gojsonschema v1.1.0 golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d - k8s.io/api v0.17.2 - k8s.io/apiextensions-apiserver v0.17.2 - k8s.io/apimachinery v0.17.2 - k8s.io/cli-runtime v0.17.2 - k8s.io/client-go v0.17.2 + k8s.io/api v0.17.3 + k8s.io/apiextensions-apiserver v0.17.3 + k8s.io/apimachinery v0.17.3 + k8s.io/cli-runtime v0.17.3 + k8s.io/client-go v0.17.3 k8s.io/klog v1.0.0 - k8s.io/kubectl v0.17.2 + k8s.io/kubectl v0.17.3 sigs.k8s.io/yaml v1.1.0 ) diff --git a/go.sum b/go.sum index cc6b8dd9c..4d8cc83a2 100644 --- a/go.sum +++ b/go.sum @@ -332,8 +332,8 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-shellwords v1.0.9 h1:eaB5JspOwiKKcHdqcjbfe5lA9cNn/4NRRtddXJCimqk= -github.com/mattn/go-shellwords v1.0.9/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= +github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= @@ -630,25 +630,27 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= -k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= -k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss= -k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs= -k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= -k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= -k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= -k8s.io/cli-runtime v0.17.2 h1:YH4txSplyGudvxjhAJeHEtXc7Tr/16clKGfN076ydGk= -k8s.io/cli-runtime v0.17.2/go.mod h1:aa8t9ziyQdbkuizkNLAw3qe3srSyWh9zlSB7zTqRNPI= -k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= -k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= -k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= -k8s.io/component-base v0.17.2 h1:0XHf+cerTvL9I5Xwn9v+0jmqzGAZI7zNydv4tL6Cw6A= -k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs= +k8s.io/api v0.17.3 h1:XAm3PZp3wnEdzekNkcmj/9Y1zdmQYJ1I4GKSBBZ8aG0= +k8s.io/api v0.17.3/go.mod h1:YZ0OTkuw7ipbe305fMpIdf3GLXZKRigjtZaV5gzC2J0= +k8s.io/apiextensions-apiserver v0.17.3 h1:WDZWkPcbgvchEdDd7ysL21GGPx3UKZQLDZXEkevT6n4= +k8s.io/apiextensions-apiserver v0.17.3/go.mod h1:CJbCyMfkKftAd/X/V6OTHYhVn7zXnDdnkUjS1h0GTeY= +k8s.io/apimachinery v0.17.3 h1:f+uZV6rm4/tHE7xXgLyToprg6xWairaClGVkm2t8omg= +k8s.io/apimachinery v0.17.3/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/apiserver v0.17.3/go.mod h1:iJtsPpu1ZpEnHaNawpSV0nYTGBhhX2dUlnn7/QS7QiY= +k8s.io/cli-runtime v0.17.3 h1:0ZlDdJgJBKsu77trRUynNiWsRuAvAVPBNaQfnt/1qtc= +k8s.io/cli-runtime v0.17.3/go.mod h1:X7idckYphH4SZflgNpOOViSxetiMj6xI0viMAjM81TA= +k8s.io/client-go v0.17.3 h1:deUna1Ksx05XeESH6XGCyONNFfiQmDdqeqUvicvP6nU= +k8s.io/client-go v0.17.3/go.mod h1:cLXlTMtWHkuK4tD360KpWz2gG2KtdWEr/OT02i3emRQ= +k8s.io/code-generator v0.17.3/go.mod h1:l8BLVwASXQZTo2xamW5mQNFCe1XPiAesVq7Y1t7PiQQ= +k8s.io/component-base v0.17.3 h1:hQzTSshY14aLSR6WGIYvmw+w+u6V4d+iDR2iDGMrlUg= +k8s.io/component-base v0.17.3/go.mod h1:GeQf4BrgelWm64PXkIXiPh/XS0hnO42d9gx9BtbZRp8= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= @@ -657,10 +659,10 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= -k8s.io/kubectl v0.17.2 h1:QZR8Q6lWiVRjwKslekdbN5WPMp53dS/17j5e+oi5XVU= -k8s.io/kubectl v0.17.2/go.mod h1:y4rfLV0n6aPmvbRCqZQjvOp3ezxsFgpqL+zF5jH/lxk= +k8s.io/kubectl v0.17.3 h1:9HHYj07kuFkM+sMJMOyQX29CKWq4lvKAG1UIPxNPMQ4= +k8s.io/kubectl v0.17.3/go.mod h1:NUn4IBY7f7yCMwSop2HCXlw/MVYP4HJBiUmOR3n9w28= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -k8s.io/metrics v0.17.2/go.mod h1:3TkNHET4ROd+NfzNxkjoVfQ0Ob4iZnaHmSEA4vYpwLw= +k8s.io/metrics v0.17.3/go.mod h1:HEJGy1fhHOjHggW9rMDBJBD3YuGroH3Y1pnIRw9FFaI= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= diff --git a/internal/completion/complete.go b/internal/completion/complete.go index a24390fc0..545f5b0dd 100644 --- a/internal/completion/complete.go +++ b/internal/completion/complete.go @@ -35,9 +35,9 @@ import ( // This should ultimately be pushed down into Cobra. // ================================================================================== -// compRequestCmd Hidden command to request completion results from the program. +// CompRequestCmd Hidden command to request completion results from the program. // Used by the shell completion script. -const compRequestCmd = "__complete" +const CompRequestCmd = "__complete" // Global map allowing to find completion functions for commands or flags. var validArgsFunctions = map[interface{}]func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective){} @@ -123,7 +123,7 @@ __helm_custom_func() done < <(compgen -W "${out[*]}" -- "$cur") fi } -`, compRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp) +`, CompRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp) } // RegisterValidArgsFunc should be called to register a function to provide argument completion for a command @@ -177,40 +177,58 @@ func (d BashCompDirective) string() string { func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { debug = settings.Debug return &cobra.Command{ - Use: fmt.Sprintf("%s [command-line]", compRequestCmd), + Use: fmt.Sprintf("%s [command-line]", CompRequestCmd), DisableFlagsInUseLine: true, Hidden: true, DisableFlagParsing: true, Args: require.MinimumNArgs(1), Short: "Request shell completion choices for the specified command-line", Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s", - compRequestCmd, "to request completion choices for the specified command-line."), + CompRequestCmd, "to request completion choices for the specified command-line."), Run: func(cmd *cobra.Command, args []string) { CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args)) - flag, trimmedArgs, toComplete, err := checkIfFlagCompletion(cmd.Root(), args[:len(args)-1], args[len(args)-1]) - if err != nil { - // Error while attempting to parse flags - CompErrorln(err.Error()) - return - } + // The last argument, which is not complete, should not be part of the list of arguments + toComplete := args[len(args)-1] + trimmedArgs := args[:len(args)-1] + // Find the real command for which completion must be performed finalCmd, finalArgs, err := cmd.Root().Find(trimmedArgs) if err != nil { // Unable to find the real command. E.g., helm invalidCmd + CompDebugln(fmt.Sprintf("Unable to find a command for arguments: %v", trimmedArgs)) return } CompDebugln(fmt.Sprintf("Found final command '%s', with finalArgs %v", finalCmd.Name(), finalArgs)) + var flag *pflag.Flag + if !finalCmd.DisableFlagParsing { + // We only do flag completion if we are allowed to parse flags + // This is important for helm plugins which need to do their own flag completion. + flag, finalArgs, toComplete, err = checkIfFlagCompletion(finalCmd, finalArgs, toComplete) + if err != nil { + // Error while attempting to parse flags + CompErrorln(err.Error()) + return + } + } + // Parse the flags and extract the arguments to prepare for calling the completion function if err = finalCmd.ParseFlags(finalArgs); err != nil { CompErrorln(fmt.Sprintf("Error while parsing flags from args %v: %s", finalArgs, err.Error())) return } - argsWoFlags := finalCmd.Flags().Args() - CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", argsWoFlags, len(argsWoFlags))) + // We only remove the flags from the arguments if DisableFlagParsing is not set. + // This is important for helm plugins, which need to receive all flags. + // The plugin completion code will do its own flag parsing. + if !finalCmd.DisableFlagParsing { + finalArgs = finalCmd.Flags().Args() + CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", finalArgs, len(finalArgs))) + } + + // Find completion function for the flag or command var key interface{} var keyStr string if flag != nil { @@ -220,26 +238,28 @@ func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command { key = finalCmd keyStr = finalCmd.Name() } - - // Find completion function for the flag or command completionFn, ok := validArgsFunctions[key] if !ok { CompErrorln(fmt.Sprintf("Dynamic completion not supported/needed for flag or command: %s", keyStr)) return } - CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), argsWoFlags, toComplete)) - completions, directive := completionFn(finalCmd, argsWoFlags, toComplete) + CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), finalArgs, toComplete)) + completions, directive := completionFn(finalCmd, finalArgs, toComplete) for _, comp := range completions { // Print each possible completion to stdout for the completion script to consume. fmt.Fprintln(out, comp) } + if directive > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp { + directive = BashCompDirectiveDefault + } + // As the last printout, print the completion directive for the // completion script to parse. // The directive integer must be that last character following a single : // The completion script expects :directive - fmt.Fprintln(out, fmt.Sprintf(":%d", directive)) + fmt.Fprintf(out, ":%d\n", directive) // Print some helpful info to stderr for the user to understand. // Output from stderr should be ignored from the completion script. @@ -252,7 +272,7 @@ func isFlag(arg string) bool { return len(arg) > 0 && arg[0] == '-' } -func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) { +func checkIfFlagCompletion(finalCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) { var flagName string trimmedArgs := args flagWithEqual := false @@ -287,19 +307,10 @@ func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string return nil, trimmedArgs, lastArg, nil } - // Find the real command for which completion must be performed - finalCmd, _, err := rootCmd.Find(trimmedArgs) - if err != nil { - // Unable to find the real command. E.g., helm invalidCmd - return nil, nil, "", errors.New("Unable to find final command for completion") - } - - CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found final command '%s'", finalCmd.Name())) - flag := findFlag(finalCmd, flagName) if flag == nil { // Flag not supported by this command, nothing to complete - err = fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName) + err := fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName) return nil, nil, "", err } @@ -357,7 +368,7 @@ func CompDebug(msg string) { if debug { // Must print to stderr for this not to be read by the completion script. - fmt.Fprintf(os.Stderr, msg) + fmt.Fprintln(os.Stderr, msg) } } @@ -378,7 +389,7 @@ func CompError(msg string) { // If not already printed by the call to CompDebug(). if !debug { // Must print to stderr for this not to be read by the completion script. - fmt.Fprintf(os.Stderr, msg) + fmt.Fprintln(os.Stderr, msg) } } diff --git a/internal/experimental/registry/client_test.go b/internal/experimental/registry/client_test.go index 33799f5fa..6e9d5db36 100644 --- a/internal/experimental/registry/client_test.go +++ b/internal/experimental/registry/client_test.go @@ -162,13 +162,13 @@ func (suite *RegistryClientTestSuite) Test_2_LoadChart() { // non-existent ref ref, err := ParseReference(fmt.Sprintf("%s/testrepo/whodis:9.9.9", suite.DockerRegistryHost)) suite.Nil(err) - ch, err := suite.RegistryClient.LoadChart(ref) + _, err = suite.RegistryClient.LoadChart(ref) suite.NotNil(err) // existing ref ref, err = ParseReference(fmt.Sprintf("%s/testrepo/testchart:1.2.3", suite.DockerRegistryHost)) suite.Nil(err) - ch, err = suite.RegistryClient.LoadChart(ref) + ch, err := suite.RegistryClient.LoadChart(ref) suite.Nil(err) suite.Equal("testchart", ch.Metadata.Name) suite.Equal("1.2.3", ch.Metadata.Version) diff --git a/pkg/action/action.go b/pkg/action/action.go index a97533696..e4db942c8 100644 --- a/pkg/action/action.go +++ b/pkg/action/action.go @@ -135,6 +135,7 @@ func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) { return c.Capabilities, nil } +// KubernetesClientSet creates a new kubernetes ClientSet based on the configuration func (c *Configuration) KubernetesClientSet() (kubernetes.Interface, error) { conf, err := c.RESTClientGetter.ToRESTConfig() if err != nil { @@ -219,7 +220,7 @@ func (c *Configuration) recordRelease(r *release.Release) { } } -// InitActionConfig initializes the action configuration +// Init initializes the action configuration func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespace string, helmDriver string, log DebugLog) error { kc := kube.New(getter) kc.Log = log @@ -240,7 +241,18 @@ func (c *Configuration) Init(getter genericclioptions.RESTClientGetter, namespac d.Log = log store = storage.Init(d) case "memory": - d := driver.NewMemory() + var d *driver.Memory + if c.Releases != nil { + if mem, ok := c.Releases.Driver.(*driver.Memory); ok { + // This function can be called more than once (e.g., helm list --all-namespaces). + // If a memory driver was already initialized, re-use it but set the possibly new namespace. + // We re-use it in case some releases where already created in the existing memory driver. + d = mem + } + } + if d == nil { + d = driver.NewMemory() + } d.SetNamespace(namespace) store = storage.Init(d) default: diff --git a/pkg/action/action_test.go b/pkg/action/action_test.go index df6a48e7f..36ef261a3 100644 --- a/pkg/action/action_test.go +++ b/pkg/action/action_test.go @@ -230,6 +230,20 @@ func withSampleTemplates() chartOption { } } +func withSampleIncludingIncorrectTemplates() chartOption { + return func(opts *chartOptions) { + sampleTemplates := []*chart.File{ + // This adds basic templates and partials. + {Name: "templates/goodbye", Data: []byte("goodbye: world")}, + {Name: "templates/empty", Data: []byte("")}, + {Name: "templates/incorrect", Data: []byte("{{ .Values.bad.doh }}")}, + {Name: "templates/with-partials", Data: []byte(`hello: {{ template "_planet" . }}`)}, + {Name: "templates/partials/_planet", Data: []byte(`{{define "_planet"}}Earth{{end}}`)}, + } + opts.Templates = append(opts.Templates, sampleTemplates...) + } +} + func withMultipleManifestTemplate() chartOption { return func(opts *chartOptions) { sampleTemplates := []*chart.File{ diff --git a/pkg/action/install.go b/pkg/action/install.go index a18549b7f..1c47a27d7 100644 --- a/pkg/action/install.go +++ b/pkg/action/install.go @@ -29,8 +29,11 @@ import ( "github.com/Masterminds/sprig/v3" "github.com/pkg/errors" + v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/resource" + "sigs.k8s.io/yaml" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/chartutil" @@ -38,6 +41,7 @@ import ( "helm.sh/helm/v3/pkg/downloader" "helm.sh/helm/v3/pkg/engine" "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/kube" kubefake "helm.sh/helm/v3/pkg/kube/fake" "helm.sh/helm/v3/pkg/postrender" "helm.sh/helm/v3/pkg/release" @@ -69,6 +73,7 @@ type Install struct { ChartPathOptions ClientOnly bool + CreateNamespace bool DryRun bool DisableHooks bool Replace bool @@ -238,11 +243,18 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // Mark this release as in-progress rel.SetStatus(release.StatusPendingInstall, "Initial install underway") + var toBeAdopted kube.ResourceList resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation) if err != nil { return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest") } + // It is safe to use "force" here because these are resources currently rendered by the chart. + err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true)) + if err != nil { + return nil, err + } + // Install requires an extra validation step of checking that resources // don't already exist before we actually create resources. If we continue // forward and create the release object with resources that already exist, @@ -250,7 +262,8 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // deleting the release because the manifest will be pointing at that // resource if !i.ClientOnly && !isUpgrade { - if err := existingResourceConflict(resources); err != nil { + toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace) + if err != nil { return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install") } } @@ -261,6 +274,32 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. return rel, nil } + if i.CreateNamespace { + ns := &v1.Namespace{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Namespace", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: i.Namespace, + Labels: map[string]string{ + "name": i.Namespace, + }, + }, + } + buf, err := yaml.Marshal(ns) + if err != nil { + return nil, err + } + resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true) + if err != nil { + return nil, err + } + if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) { + return nil, err + } + } + // If Replace is true, we need to supercede the last release. if i.Replace { if err := i.replaceRelease(rel); err != nil { @@ -287,8 +326,14 @@ func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release. // At this point, we can do the install. Note that before we were detecting whether to // do an update, but it's not clear whether we WANT to do an update if the re-use is set // to true, since that is basically an upgrade operation. - if _, err := i.cfg.KubeClient.Create(resources); err != nil { - return i.failRelease(rel, err) + if len(toBeAdopted) == 0 { + if _, err := i.cfg.KubeClient.Create(resources); err != nil { + return i.failRelease(rel, err) + } + } else { + if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil { + return i.failRelease(rel, err) + } } if i.Wait { @@ -710,6 +755,7 @@ func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) ( Getters: getter.All(settings), Options: []getter.Option{ getter.WithBasicAuth(c.Username, c.Password), + getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile), }, RepositoryConfig: settings.RepositoryConfig, RepositoryCache: settings.RepositoryCache, diff --git a/pkg/action/install_test.go b/pkg/action/install_test.go index ba350819d..bf47895a1 100644 --- a/pkg/action/install_test.go +++ b/pkg/action/install_test.go @@ -240,6 +240,21 @@ func TestInstallRelease_DryRun(t *testing.T) { is.Equal(res.Info.Description, "Dry run complete") } +func TestInstallReleaseIncorrectTemplate_DryRun(t *testing.T) { + is := assert.New(t) + instAction := installAction(t) + instAction.DryRun = true + vals := map[string]interface{}{} + _, err := instAction.Run(buildChart(withSampleIncludingIncorrectTemplates()), vals) + expectedErr := "\"hello/templates/incorrect\" at <.Values.bad.doh>: nil pointer evaluating interface {}.doh" + if err == nil { + t.Fatalf("Install should fail containing error: %s", expectedErr) + } + if err != nil { + is.Contains(err.Error(), expectedErr) + } +} + func TestInstallRelease_NoHooks(t *testing.T) { is := assert.New(t) instAction := installAction(t) diff --git a/pkg/action/lint.go b/pkg/action/lint.go index ddb0101c7..2292c14bf 100644 --- a/pkg/action/lint.go +++ b/pkg/action/lint.go @@ -38,6 +38,7 @@ type Lint struct { WithSubcharts bool } +// LintResult is the result of Lint type LintResult struct { TotalChartsLinted int Messages []support.Message diff --git a/pkg/action/list.go b/pkg/action/list.go index 5be60ac42..ac6fd1b75 100644 --- a/pkg/action/list.go +++ b/pkg/action/list.go @@ -165,6 +165,10 @@ func (l *List) Run() ([]*release.Release, error) { return true }) + if err != nil { + return nil, err + } + if results == nil { return results, nil } @@ -238,7 +242,7 @@ func filterList(releases []*release.Release) []*release.Release { return list } -// setStateMask calculates the state mask based on parameters. +// SetStateMask calculates the state mask based on parameters. func (l *List) SetStateMask() { if l.All { l.StateMask = ListAll diff --git a/pkg/action/package.go b/pkg/action/package.go index a8823ca13..f9cc19e48 100644 --- a/pkg/action/package.go +++ b/pkg/action/package.go @@ -118,6 +118,7 @@ func setVersion(ch *chart.Chart, ver string) error { return nil } +// Clearsign signs a chart func (p *Package) Clearsign(filename string) error { // Load keyring signer, err := provenance.NewFromKeyring(p.Keyring, p.Key) diff --git a/pkg/action/pull.go b/pkg/action/pull.go index 4ff5f5c3e..ee20bbe83 100644 --- a/pkg/action/pull.go +++ b/pkg/action/pull.go @@ -101,7 +101,11 @@ func (p *Pull) Run(chartRef string) (string, error) { } if p.Verify { - fmt.Fprintf(&out, "Verification: %v\n", v) + for name := range v.SignedBy.Identities { + fmt.Fprintf(&out, "Signed by: %v\n", name) + } + fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", v.SignedBy.PrimaryKey.Fingerprint) + fmt.Fprintf(&out, "Chart Hash Verified: %s\n", v.FileHash) } // After verification, untar the chart into the requested directory. diff --git a/pkg/action/show.go b/pkg/action/show.go index b29107d4e..cc85477cd 100644 --- a/pkg/action/show.go +++ b/pkg/action/show.go @@ -27,12 +27,17 @@ import ( "helm.sh/helm/v3/pkg/chartutil" ) +// ShowOutputFormat is the format of the output of `helm show` type ShowOutputFormat string const ( - ShowAll ShowOutputFormat = "all" - ShowChart ShowOutputFormat = "chart" + // ShowAll is the format which shows all the information of a chart + ShowAll ShowOutputFormat = "all" + // ShowChart is the format which only shows the chart's definition + ShowChart ShowOutputFormat = "chart" + // ShowValues is the format which only shows the chart's values ShowValues ShowOutputFormat = "values" + // ShowReadme is the format which only shows the chart's README ShowReadme ShowOutputFormat = "readme" ) @@ -46,8 +51,9 @@ func (o ShowOutputFormat) String() string { // // It provides the implementation of 'helm show' and its respective subcommands. type Show struct { - OutputFormat ShowOutputFormat ChartPathOptions + Devel bool + OutputFormat ShowOutputFormat } // NewShow creates a new Show object with the given configuration. diff --git a/pkg/action/upgrade.go b/pkg/action/upgrade.go index f5153f07e..47c3ca0de 100644 --- a/pkg/action/upgrade.go +++ b/pkg/action/upgrade.go @@ -42,9 +42,11 @@ type Upgrade struct { ChartPathOptions - Install bool - Devel bool - Namespace string + Install bool + Devel bool + Namespace string + // SkipCRDs skip installing CRDs when install flag is enabled during upgrade + SkipCRDs bool Timeout time.Duration Wait bool DisableHooks bool @@ -55,12 +57,13 @@ type Upgrade struct { // Recreate will (if true) recreate pods after a rollback. Recreate bool // MaxHistory limits the maximum number of revisions saved per release - MaxHistory int - Atomic bool - CleanupOnFail bool - SubNotes bool - Description string - PostRenderer postrender.PostRenderer + MaxHistory int + Atomic bool + CleanupOnFail bool + SubNotes bool + Description string + PostRenderer postrender.PostRenderer + DisableOpenAPIValidation bool } // NewUpgrade creates a new Upgrade object with the given configuration. @@ -72,6 +75,10 @@ func NewUpgrade(cfg *Configuration) *Upgrade { // Run executes the upgrade on the given release. func (u *Upgrade) Run(name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + if err := u.cfg.KubeClient.IsReachable(); err != nil { + return nil, err + } + // Make sure if Atomic is set, that wait is set as well. This makes it so // the user doesn't have to specify both u.Wait = u.Wait || u.Atomic @@ -184,7 +191,7 @@ func (u *Upgrade) prepareUpgrade(name string, chart *chart.Chart, vals map[strin if len(notesTxt) > 0 { upgradedRelease.Info.Notes = notesTxt } - err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes()) + err = validateManifest(u.cfg.KubeClient, manifestDoc.Bytes(), !u.DisableOpenAPIValidation) return currentRelease, upgradedRelease, err } @@ -193,11 +200,17 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea if err != nil { return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from current release manifest") } - target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), true) + target, err := u.cfg.KubeClient.Build(bytes.NewBufferString(upgradedRelease.Manifest), !u.DisableOpenAPIValidation) if err != nil { return upgradedRelease, errors.Wrap(err, "unable to build kubernetes objects from new release manifest") } + // It is safe to use force only on target because these are resources currently rendered by the chart. + err = target.Visit(setMetadataVisitor(upgradedRelease.Name, upgradedRelease.Namespace, true)) + if err != nil { + return upgradedRelease, err + } + // Do a basic diff using gvk + name to figure out what new resources are being created so we can validate they don't already exist existingResources := make(map[string]bool) for _, r := range current { @@ -211,10 +224,19 @@ func (u *Upgrade) performUpgrade(originalRelease, upgradedRelease *release.Relea } } - if err := existingResourceConflict(toBeCreated); err != nil { - return nil, errors.Wrap(err, "rendered manifests contain a new resource that already exists. Unable to continue with update") + toBeUpdated, err := existingResourceConflict(toBeCreated, upgradedRelease.Name, upgradedRelease.Namespace) + if err != nil { + return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with update") } + toBeUpdated.Visit(func(r *resource.Info, err error) error { + if err != nil { + return err + } + current.Append(r) + return nil + }) + if u.DryRun { u.cfg.Log("dry run for %s", upgradedRelease.Name) if len(u.Description) > 0 { @@ -379,8 +401,8 @@ func (u *Upgrade) reuseValues(chart *chart.Chart, current *release.Release, newV return newVals, nil } -func validateManifest(c kube.Interface, manifest []byte) error { - _, err := c.Build(bytes.NewReader(manifest), true) +func validateManifest(c kube.Interface, manifest []byte, openAPIValidation bool) error { + _, err := c.Build(bytes.NewReader(manifest), openAPIValidation) return err } diff --git a/pkg/action/validate.go b/pkg/action/validate.go index 6bbfc5e8d..0c40a9c3c 100644 --- a/pkg/action/validate.go +++ b/pkg/action/validate.go @@ -21,12 +21,25 @@ import ( "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/resource" "helm.sh/helm/v3/pkg/kube" ) -func existingResourceConflict(resources kube.ResourceList) error { +var accessor = meta.NewAccessor() + +const ( + appManagedByLabel = "app.kubernetes.io/managed-by" + appManagedByHelm = "Helm" + helmReleaseNameAnnotation = "meta.helm.sh/release-name" + helmReleaseNamespaceAnnotation = "meta.helm.sh/release-namespace" +) + +func existingResourceConflict(resources kube.ResourceList, releaseName, releaseNamespace string) (kube.ResourceList, error) { + var requireUpdate kube.ResourceList + err := resources.Visit(func(info *resource.Info, err error) error { if err != nil { return err @@ -38,11 +51,134 @@ func existingResourceConflict(resources kube.ResourceList) error { if apierrors.IsNotFound(err) { return nil } - return errors.Wrap(err, "could not get information about the resource") } - return fmt.Errorf("existing resource conflict: namespace: %s, name: %s, existing_kind: %s, new_kind: %s", info.Namespace, info.Name, existing.GetObjectKind().GroupVersionKind(), info.Mapping.GroupVersionKind) + // Allow adoption of the resource if it is managed by Helm and is annotated with correct release name and namespace. + if err := checkOwnership(existing, releaseName, releaseNamespace); err != nil { + return fmt.Errorf("%s exists and cannot be imported into the current release: %s", resourceString(info), err) + } + + requireUpdate.Append(info) + return nil }) - return err + + return requireUpdate, err +} + +func checkOwnership(obj runtime.Object, releaseName, releaseNamespace string) error { + lbls, err := accessor.Labels(obj) + if err != nil { + return err + } + annos, err := accessor.Annotations(obj) + if err != nil { + return err + } + + var errs []error + if err := requireValue(lbls, appManagedByLabel, appManagedByHelm); err != nil { + errs = append(errs, fmt.Errorf("label validation error: %s", err)) + } + if err := requireValue(annos, helmReleaseNameAnnotation, releaseName); err != nil { + errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) + } + if err := requireValue(annos, helmReleaseNamespaceAnnotation, releaseNamespace); err != nil { + errs = append(errs, fmt.Errorf("annotation validation error: %s", err)) + } + + if len(errs) > 0 { + err := errors.New("invalid ownership metadata") + for _, e := range errs { + err = fmt.Errorf("%w; %s", err, e) + } + return err + } + + return nil +} + +func requireValue(meta map[string]string, k, v string) error { + actual, ok := meta[k] + if !ok { + return fmt.Errorf("missing key %q: must be set to %q", k, v) + } + if actual != v { + return fmt.Errorf("key %q must equal %q: current value is %q", k, v, actual) + } + return nil +} + +// setMetadataVisitor adds release tracking metadata to all resources. If force is enabled, existing +// ownership metadata will be overwritten. Otherwise an error will be returned if any resource has an +// existing and conflicting value for the managed by label or Helm release/namespace annotations. +func setMetadataVisitor(releaseName, releaseNamespace string, force bool) resource.VisitorFunc { + return func(info *resource.Info, err error) error { + if err != nil { + return err + } + + if !force { + if err := checkOwnership(info.Object, releaseName, releaseNamespace); err != nil { + return fmt.Errorf("%s cannot be owned: %s", resourceString(info), err) + } + } + + if err := mergeLabels(info.Object, map[string]string{ + appManagedByLabel: appManagedByHelm, + }); err != nil { + return fmt.Errorf( + "%s labels could not be updated: %s", + resourceString(info), err, + ) + } + + if err := mergeAnnotations(info.Object, map[string]string{ + helmReleaseNameAnnotation: releaseName, + helmReleaseNamespaceAnnotation: releaseNamespace, + }); err != nil { + return fmt.Errorf( + "%s annotations could not be updated: %s", + resourceString(info), err, + ) + } + + return nil + } +} + +func resourceString(info *resource.Info) string { + _, k := info.Mapping.GroupVersionKind.ToAPIVersionAndKind() + return fmt.Sprintf( + "%s %q in namespace %q", + k, info.Name, info.Namespace, + ) +} + +func mergeLabels(obj runtime.Object, labels map[string]string) error { + current, err := accessor.Labels(obj) + if err != nil { + return err + } + return accessor.SetLabels(obj, mergeStrStrMaps(current, labels)) +} + +func mergeAnnotations(obj runtime.Object, annotations map[string]string) error { + current, err := accessor.Annotations(obj) + if err != nil { + return err + } + return accessor.SetAnnotations(obj, mergeStrStrMaps(current, annotations)) +} + +// merge two maps, always taking the value on the right +func mergeStrStrMaps(current, desired map[string]string) map[string]string { + result := make(map[string]string) + for k, v := range current { + result[k] = v + } + for k, desiredVal := range desired { + result[k] = desiredVal + } + return result } diff --git a/pkg/action/validate_test.go b/pkg/action/validate_test.go new file mode 100644 index 000000000..a9c1cb49c --- /dev/null +++ b/pkg/action/validate_test.go @@ -0,0 +1,123 @@ +/* +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 action + +import ( + "testing" + + "helm.sh/helm/v3/pkg/kube" + + appsv1 "k8s.io/api/apps/v1" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/meta" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/resource" +) + +func newDeploymentResource(name, namespace string) *resource.Info { + return &resource.Info{ + Name: name, + Mapping: &meta.RESTMapping{ + Resource: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployment"}, + GroupVersionKind: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}, + }, + Object: &appsv1.Deployment{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, + } +} + +func TestCheckOwnership(t *testing.T) { + deployFoo := newDeploymentResource("foo", "ns-a") + + // Verify that a resource that lacks labels/annotations is not owned + err := checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; label validation error: missing key "app.kubernetes.io/managed-by": must be set to "Helm"; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) + + // Set managed by label and verify annotation error message + _ = accessor.SetLabels(deployFoo.Object, map[string]string{ + appManagedByLabel: appManagedByHelm, + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-name": must be set to "rel-a"; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) + + // Set only the release name annotation and verify missing release namespace error message + _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ + helmReleaseNameAnnotation: "rel-a", + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: missing key "meta.helm.sh/release-namespace": must be set to "ns-a"`) + + // Set both release name and namespace annotations and verify no ownership errors + _ = accessor.SetAnnotations(deployFoo.Object, map[string]string{ + helmReleaseNameAnnotation: "rel-a", + helmReleaseNamespaceAnnotation: "ns-a", + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.NoError(t, err) + + // Verify ownership error for wrong release name + err = checkOwnership(deployFoo.Object, "rel-b", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-name" must equal "rel-b": current value is "rel-a"`) + + // Verify ownership error for wrong release namespace + err = checkOwnership(deployFoo.Object, "rel-a", "ns-b") + assert.EqualError(t, err, `invalid ownership metadata; annotation validation error: key "meta.helm.sh/release-namespace" must equal "ns-b": current value is "ns-a"`) + + // Verify ownership error for wrong manager label + _ = accessor.SetLabels(deployFoo.Object, map[string]string{ + appManagedByLabel: "helm", + }) + err = checkOwnership(deployFoo.Object, "rel-a", "ns-a") + assert.EqualError(t, err, `invalid ownership metadata; label validation error: key "app.kubernetes.io/managed-by" must equal "Helm": current value is "helm"`) +} + +func TestSetMetadataVisitor(t *testing.T) { + var ( + err error + deployFoo = newDeploymentResource("foo", "ns-a") + deployBar = newDeploymentResource("bar", "ns-a-system") + resources = kube.ResourceList{deployFoo, deployBar} + ) + + // Set release tracking metadata and verify no error + err = resources.Visit(setMetadataVisitor("rel-a", "ns-a", true)) + assert.NoError(t, err) + + // Verify that release "b" cannot take ownership of "a" + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) + assert.Error(t, err) + + // Force release "b" to take ownership + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", true)) + assert.NoError(t, err) + + // Check that there is now no ownership error when setting metadata without force + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) + assert.NoError(t, err) + + // Add a new resource that is missing ownership metadata and verify error + resources.Append(newDeploymentResource("baz", "default")) + err = resources.Visit(setMetadataVisitor("rel-b", "ns-a", false)) + assert.Error(t, err) + assert.Contains(t, err.Error(), `Deployment "baz" in namespace "" cannot be owned`) +} diff --git a/pkg/action/verify.go b/pkg/action/verify.go index c66b14b47..f36239496 100644 --- a/pkg/action/verify.go +++ b/pkg/action/verify.go @@ -17,6 +17,9 @@ limitations under the License. package action import ( + "fmt" + "strings" + "helm.sh/helm/v3/pkg/downloader" ) @@ -25,6 +28,7 @@ import ( // It provides the implementation of 'helm verify'. type Verify struct { Keyring string + Out string } // NewVerify creates a new Verify object with the given configuration. @@ -34,6 +38,22 @@ func NewVerify() *Verify { // Run executes 'helm verify'. func (v *Verify) Run(chartfile string) error { - _, err := downloader.VerifyChart(chartfile, v.Keyring) - return err + var out strings.Builder + p, err := downloader.VerifyChart(chartfile, v.Keyring) + if err != nil { + return err + } + + for name := range p.SignedBy.Identities { + fmt.Fprintf(&out, "Signed by: %v\n", name) + } + fmt.Fprintf(&out, "Using Key With Fingerprint: %X\n", p.SignedBy.PrimaryKey.Fingerprint) + fmt.Fprintf(&out, "Chart Hash Verified: %s\n", p.FileHash) + + // TODO(mattfarina): The output is set as a property rather than returned + // to maintain the Go API. In Helm v4 this function should return the out + // and the property on the struct can be removed. + v.Out = out.String() + + return nil } diff --git a/pkg/chartutil/dependencies.go b/pkg/chartutil/dependencies.go index e1aeb6160..c7dd63745 100644 --- a/pkg/chartutil/dependencies.go +++ b/pkg/chartutil/dependencies.go @@ -165,6 +165,17 @@ Loop: cd = append(cd, n) } } + // don't keep disabled charts in metadata + cdMetadata := []*chart.Dependency{} + copy(cdMetadata, c.Metadata.Dependencies[:0]) + for _, n := range c.Metadata.Dependencies { + if _, ok := rm[n.Name]; !ok { + cdMetadata = append(cdMetadata, n) + } + } + // set the correct dependencies in metadata + c.Metadata.Dependencies = nil + c.Metadata.Dependencies = append(c.Metadata.Dependencies, cdMetadata...) c.SetDependencies(cd...) return nil @@ -213,7 +224,7 @@ func processImportValues(c *chart.Chart, cvals Values) error { // get child table vv, err := cvals.Table(r.Name + "." + child) if err != nil { - log.Printf("Warning: ImportValues missing table: %v", err) + log.Printf("Warning: ImportValues missing table from chart %s: %v", r.Name, err) continue } // create value map from child to be merged into parent diff --git a/pkg/chartutil/dependencies_test.go b/pkg/chartutil/dependencies_test.go index df8f6798c..469d8c46e 100644 --- a/pkg/chartutil/dependencies_test.go +++ b/pkg/chartutil/dependencies_test.go @@ -273,6 +273,36 @@ func TestProcessDependencyImportValues(t *testing.T) { } } +func TestProcessDependencyImportValuesForEnabledCharts(t *testing.T) { + c := loadChart(t, "testdata/import-values-from-enabled-subchart/parent-chart") + nameOverride := "parent-chart-prod" + + if err := processDependencyImportValues(c); err != nil { + t.Fatalf("processing import values dependencies %v", err) + } + + if len(c.Dependencies()) != 2 { + t.Fatalf("expected 2 dependencies for this chart, but got %d", len(c.Dependencies())) + } + + if err := processDependencyEnabled(c, c.Values, ""); err != nil { + t.Fatalf("expected no errors but got %q", err) + } + + if len(c.Dependencies()) != 1 { + t.Fatal("expected no changes in dependencies") + } + + if len(c.Metadata.Dependencies) != 1 { + t.Fatalf("expected 1 dependency specified in Chart.yaml, got %d", len(c.Metadata.Dependencies)) + } + + prodDependencyValues := c.Dependencies()[0].Values + if prodDependencyValues["nameOverride"] != nameOverride { + t.Fatalf("dependency chart name should be %s but got %s", nameOverride, prodDependencyValues["nameOverride"]) + } +} + func TestGetAliasDependency(t *testing.T) { c := loadChart(t, "testdata/frobnitz") req := c.Metadata.Dependencies diff --git a/pkg/chartutil/expand_test.go b/pkg/chartutil/expand_test.go index 0eb35aedb..9a85e3247 100644 --- a/pkg/chartutil/expand_test.go +++ b/pkg/chartutil/expand_test.go @@ -39,19 +39,6 @@ func TestExpand(t *testing.T) { t.Fatal(err) } - files, err := ioutil.ReadDir(dest) - if err != nil { - t.Fatalf("error reading output directory %s: %s", dest, err) - } - - if len(files) != 1 { - t.Fatalf("expected a single chart directory in output directory %s", dest) - } - - if !files[0].IsDir() { - t.Fatalf("expected a chart directory in output directory %s", dest) - } - expectedChartPath := filepath.Join(dest, "frobnitz") fi, err := os.Stat(expectedChartPath) if err != nil { @@ -81,8 +68,14 @@ func TestExpand(t *testing.T) { if err != nil { t.Fatal(err) } - if fi.Size() != expect.Size() { - t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat alwaty returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } } } } @@ -127,8 +120,14 @@ func TestExpandFile(t *testing.T) { if err != nil { t.Fatal(err) } - if fi.Size() != expect.Size() { - t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + // os.Stat can return different values for directories, based on the OS + // for Linux, for example, os.Stat alwaty returns the size of the directory + // (value-4096) regardless of the size of the contents of the directory + mode := expect.Mode() + if !mode.IsDir() { + if fi.Size() != expect.Size() { + t.Errorf("Expected %s to have size %d, got %d", fi.Name(), expect.Size(), fi.Size()) + } } } } diff --git a/pkg/chartutil/save.go b/pkg/chartutil/save.go index 84b4a47df..b4337e296 100644 --- a/pkg/chartutil/save.go +++ b/pkg/chartutil/save.go @@ -161,6 +161,20 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { return err } + // Save Chart.lock + // TODO: remove the APIVersion check when APIVersionV1 is not used anymore + if c.Metadata.APIVersion == chart.APIVersionV2 { + if c.Lock != nil { + ldata, err := yaml.Marshal(c.Lock) + if err != nil { + return err + } + if err := writeToTar(out, filepath.Join(base, "Chart.lock"), ldata); err != nil { + return err + } + } + } + // Save values.yaml for _, f := range c.Raw { if f.Name == ValuesfileName { @@ -217,7 +231,7 @@ func writeTarContents(out *tar.Writer, c *chart.Chart, prefix string) error { func writeToTar(out *tar.Writer, name string, body []byte) error { // TODO: Do we need to create dummy parent directory names if none exist? h := &tar.Header{ - Name: name, + Name: filepath.ToSlash(name), Mode: 0644, Size: int64(len(body)), ModTime: time.Now(), diff --git a/pkg/chartutil/save_test.go b/pkg/chartutil/save_test.go index 952b636e6..58fb8a47d 100644 --- a/pkg/chartutil/save_test.go +++ b/pkg/chartutil/save_test.go @@ -49,6 +49,9 @@ func TestSave(t *testing.T) { Name: "ahab", Version: "1.2.3", }, + Lock: &chart.Lock{ + Digest: "testdigest", + }, Files: []*chart.File{ {Name: "scheherazade/shahryar.txt", Data: []byte("1,001 Nights")}, }, @@ -77,6 +80,9 @@ func TestSave(t *testing.T) { if len(c2.Files) != 1 || c2.Files[0].Name != "scheherazade/shahryar.txt" { t.Fatal("Files data did not match") } + if c2.Lock != nil { + t.Fatal("Expected v1 chart archive not to contain Chart.lock file") + } if !bytes.Equal(c.Schema, c2.Schema) { indentation := 4 @@ -87,6 +93,22 @@ func TestSave(t *testing.T) { if _, err := Save(&chartWithInvalidJSON, dest); err == nil { t.Fatalf("Invalid JSON was not caught while saving chart") } + + c.Metadata.APIVersion = chart.APIVersionV2 + where, err = Save(c, dest) + if err != nil { + t.Fatalf("Failed to save: %s", err) + } + c2, err = loader.LoadFile(where) + if err != nil { + t.Fatal(err) + } + if c2.Lock == nil { + t.Fatal("Expected v2 chart archive to containe a Chart.lock file") + } + if c2.Lock.Digest != c.Lock.Digest { + t.Fatal("Chart.lock data did not match") + } }) } } diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock new file mode 100644 index 000000000..b2f17fb39 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.lock @@ -0,0 +1,9 @@ +dependencies: +- name: dev + repository: file://envs/dev + version: v0.1.0 +- name: prod + repository: file://envs/prod + version: v0.1.0 +digest: sha256:9403fc24f6cf9d6055820126cf7633b4bd1fed3c77e4880c674059f536346182 +generated: "2020-02-03T10:38:51.180474+01:00" diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml new file mode 100644 index 000000000..24b26d9e5 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/Chart.yaml @@ -0,0 +1,22 @@ +apiVersion: v2 +name: parent-chart +version: v0.1.0 +appVersion: v0.1.0 +dependencies: + - name: dev + repository: "file://envs/dev" + version: ">= 0.0.1" + condition: dev.enabled,global.dev.enabled + tags: + - dev + import-values: + - data + + - name: prod + repository: "file://envs/prod" + version: ">= 0.0.1" + condition: prod.enabled,global.prod.enabled + tags: + - prod + import-values: + - data \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz new file mode 100644 index 000000000..d28e1621c Binary files /dev/null and b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/dev-v0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz new file mode 100644 index 000000000..a0c5aa84b Binary files /dev/null and b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/charts/prod-v0.1.0.tgz differ diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml new file mode 100644 index 000000000..80a52f538 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: dev +version: v0.1.0 +appVersion: v0.1.0 \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml new file mode 100644 index 000000000..38f03484d --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/dev/values.yaml @@ -0,0 +1,9 @@ +# Dev values parent-chart +nameOverride: parent-chart-dev +exports: + data: + resources: + autoscaler: + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml new file mode 100644 index 000000000..bda4be458 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v2 +name: prod +version: v0.1.0 +appVersion: v0.1.0 \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml new file mode 100644 index 000000000..10cc756b2 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/envs/prod/values.yaml @@ -0,0 +1,9 @@ +# Prod values parent-chart +nameOverride: parent-chart-prod +exports: + data: + resources: + autoscaler: + minReplicas: 2 + maxReplicas: 5 + targetCPUUtilizationPercentage: 90 diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml new file mode 100644 index 000000000..976e5a8f1 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/templates/autoscaler.yaml @@ -0,0 +1,16 @@ +################################################################################################### +# parent-chart horizontal pod autoscaler +################################################################################################### +apiVersion: autoscaling/v1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ .Release.Name }}-autoscaler + namespace: {{ .Release.Namespace }} +spec: + scaleTargetRef: + apiVersion: apps/v1beta1 + kind: Deployment + name: {{ .Release.Name }} + minReplicas: {{ required "A valid .Values.resources.autoscaler.minReplicas entry required!" .Values.resources.autoscaler.minReplicas }} + maxReplicas: {{ required "A valid .Values.resources.autoscaler.maxReplicas entry required!" .Values.resources.autoscaler.maxReplicas }} + targetCPUUtilizationPercentage: {{ required "A valid .Values.resources.autoscaler.targetCPUUtilizationPercentage!" .Values.resources.autoscaler.targetCPUUtilizationPercentage }} \ No newline at end of file diff --git a/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml new file mode 100644 index 000000000..b812f0a33 --- /dev/null +++ b/pkg/chartutil/testdata/import-values-from-enabled-subchart/parent-chart/values.yaml @@ -0,0 +1,10 @@ +# Default values for parent-chart. +nameOverride: parent-chart +tags: + dev: false + prod: true +resources: + autoscaler: + minReplicas: 0 + maxReplicas: 0 + targetCPUUtilizationPercentage: 99 \ No newline at end of file diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index 5f947aec7..e279331b0 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -46,6 +46,10 @@ type EnvSettings struct { KubeConfig string // KubeContext is the name of the kubeconfig context. KubeContext string + // Bearer KubeToken used for authentication + KubeToken string + // Kubernetes API Server Endpoint for authentication + KubeAPIServer string // Debug indicates whether or not Helm is running in Debug mode. Debug bool // RegistryConfig is the path to the registry config file. @@ -63,6 +67,8 @@ func New() *EnvSettings { env := EnvSettings{ namespace: os.Getenv("HELM_NAMESPACE"), KubeContext: os.Getenv("HELM_KUBECONTEXT"), + KubeToken: os.Getenv("HELM_KUBETOKEN"), + KubeAPIServer: os.Getenv("HELM_KUBEAPISERVER"), PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")), RegistryConfig: envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")), RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), @@ -77,6 +83,8 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.StringVarP(&s.namespace, "namespace", "n", s.namespace, "namespace scope for this request") fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file") fs.StringVar(&s.KubeContext, "kube-context", s.KubeContext, "name of the kubeconfig context to use") + fs.StringVar(&s.KubeToken, "kube-token", s.KubeToken, "bearer token used for authentication") + fs.StringVar(&s.KubeAPIServer, "kube-apiserver", s.KubeAPIServer, "the address and the port for the Kubernetes API server") fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output") fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file") fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs") @@ -100,6 +108,8 @@ func (s *EnvSettings) EnvVars() map[string]string { "HELM_REPOSITORY_CONFIG": s.RepositoryConfig, "HELM_NAMESPACE": s.Namespace(), "HELM_KUBECONTEXT": s.KubeContext, + "HELM_KUBETOKEN": s.KubeToken, + "HELM_KUBEAPISERVER": s.KubeAPIServer, } if s.KubeConfig != "" { @@ -124,7 +134,15 @@ func (s *EnvSettings) Namespace() string { //RESTClientGetter gets the kubeconfig from EnvSettings func (s *EnvSettings) RESTClientGetter() genericclioptions.RESTClientGetter { s.configOnce.Do(func() { - s.config = kube.GetConfig(s.KubeConfig, s.KubeContext, s.namespace) + clientConfig := kube.GetConfig(s.KubeConfig, s.KubeContext, s.namespace) + if s.KubeToken != "" { + clientConfig.BearerToken = &s.KubeToken + } + if s.KubeAPIServer != "" { + clientConfig.APIServer = &s.KubeAPIServer + } + + s.config = clientConfig }) return s.config } diff --git a/pkg/downloader/chart_downloader.go b/pkg/downloader/chart_downloader.go index f3d4321c5..340a65472 100644 --- a/pkg/downloader/chart_downloader.go +++ b/pkg/downloader/chart_downloader.go @@ -181,8 +181,10 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er c.Options = append( c.Options, getter.WithURL(rc.URL), - getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile), ) + if rc.CertFile != "" || rc.KeyFile != "" || rc.CAFile != "" { + c.Options = append(c.Options, getter.WithTLSClientConfig(rc.CertFile, rc.KeyFile, rc.CAFile)) + } if rc.Username != "" && rc.Password != "" { c.Options = append( c.Options, @@ -210,12 +212,14 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er if err != nil { return u, err } - if r != nil && r.Config != nil && r.Config.Username != "" && r.Config.Password != "" { - c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password)) - } - if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" { - c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) + if r != nil && r.Config != nil { + if r.Config.CertFile != "" || r.Config.KeyFile != "" || r.Config.CAFile != "" { + c.Options = append(c.Options, getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile)) + } + if r.Config.Username != "" && r.Config.Password != "" { + c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password)) + } } // Next, we need to load the index, and actually look up the chart. @@ -255,9 +259,6 @@ func (c *ChartDownloader) ResolveChartVersion(ref, version string) (*url.URL, er if _, err := getter.NewHTTPGetter(getter.WithURL(rc.URL)); err != nil { return repoURL, err } - if r != nil && r.Config != nil && r.Config.Username != "" && r.Config.Password != "" { - c.Options = append(c.Options, getter.WithBasicAuth(r.Config.Username, r.Config.Password)) - } return u, err } diff --git a/pkg/downloader/chart_downloader_test.go b/pkg/downloader/chart_downloader_test.go index e0692c8c8..abfb007ff 100644 --- a/pkg/downloader/chart_downloader_test.go +++ b/pkg/downloader/chart_downloader_test.go @@ -227,6 +227,58 @@ func TestDownloadTo(t *testing.T) { } } +func TestDownloadTo_TLS(t *testing.T) { + // Set up mock server w/ tls enabled + srv, err := repotest.NewTempServer("testdata/*.tgz*") + srv.Stop() + if err != nil { + t.Fatal(err) + } + srv.StartTLS() + defer srv.Stop() + if err := srv.CreateIndex(); err != nil { + t.Fatal(err) + } + if err := srv.LinkIndices(); err != nil { + t.Fatal(err) + } + + repoConfig := filepath.Join(srv.Root(), "repositories.yaml") + repoCache := srv.Root() + + c := ChartDownloader{ + Out: os.Stderr, + Verify: VerifyAlways, + Keyring: "testdata/helm-test-key.pub", + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + Getters: getter.All(&cli.EnvSettings{ + RepositoryConfig: repoConfig, + RepositoryCache: repoCache, + }), + Options: []getter.Option{}, + } + cname := "test/signtest" + dest := srv.Root() + where, v, err := c.DownloadTo(cname, "", dest) + if err != nil { + t.Fatal(err) + } + + target := filepath.Join(dest, "signtest-0.1.0.tgz") + if expect := target; where != expect { + t.Errorf("Expected download to %s, got %s", expect, where) + } + + if v.FileHash == "" { + t.Error("File hash was empty, but verification is required.") + } + + if _, err := os.Stat(target); err != nil { + t.Error(err) + } +} + func TestDownloadTo_VerifyLater(t *testing.T) { defer ensure.HelmHome(t)() diff --git a/pkg/downloader/manager.go b/pkg/downloader/manager.go index cb139f824..ff451a6e8 100644 --- a/pkg/downloader/manager.go +++ b/pkg/downloader/manager.go @@ -82,6 +82,19 @@ func (m *Manager) Build() error { // Check that all of the repos we're dependent on actually exist. req := c.Metadata.Dependencies + + // If using apiVersion v1, calculate the hash before resolve repo names + // because resolveRepoNames will change req if req uses repo alias + // and Helm 2 calculate the digest from the original req + // Fix for: https://github.com/helm/helm/issues/7619 + var v2Sum string + if c.Metadata.APIVersion == chart.APIVersionV1 { + v2Sum, err = resolver.HashV2Req(req) + if err != nil { + return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies") + } + } + if _, err := m.resolveRepoNames(req); err != nil { return err } @@ -92,7 +105,7 @@ func (m *Manager) Build() error { // Fix for: https://github.com/helm/helm/issues/7233 if c.Metadata.APIVersion == chart.APIVersionV1 { log.Println("warning: a valid Helm v3 hash was not found. Checking against Helm v2 hash...") - if sum, err := resolver.HashV2Req(req); err != nil || sum != lock.Digest { + if v2Sum != lock.Digest { return errors.New("the lock file (requirements.lock) is out of sync with the dependencies file (requirements.yaml). Please update the dependencies") } } else { diff --git a/pkg/gates/gates_test.go b/pkg/gates/gates_test.go index a71d4905b..6bdd17ed6 100644 --- a/pkg/gates/gates_test.go +++ b/pkg/gates/gates_test.go @@ -45,3 +45,12 @@ func TestError(t *testing.T) { t.Errorf("incorrect error message. Received %s", g.Error().Error()) } } + +func TestString(t *testing.T) { + os.Unsetenv(name) + g := Gate(name) + + if g.String() != "HELM_EXPERIMENTAL_FEATURE" { + t.Errorf("incorrect string representation. Received %s", g.String()) + } +} diff --git a/pkg/getter/getter.go b/pkg/getter/getter.go index 68638c2ca..4ccc74834 100644 --- a/pkg/getter/getter.go +++ b/pkg/getter/getter.go @@ -28,13 +28,14 @@ import ( // // Getters may or may not ignore these parameters as they are passed in. type options struct { - url string - certFile string - keyFile string - caFile string - username string - password string - userAgent string + url string + certFile string + keyFile string + caFile string + insecureSkipVerifyTLS bool + username string + password string + userAgent string } // Option allows specifying various settings configurable by the user for overriding the defaults @@ -64,6 +65,13 @@ func WithUserAgent(userAgent string) Option { } } +// WithInsecureSkipVerifyTLS determines if a TLS Certificate will be checked +func WithInsecureSkipVerifyTLS(insecureSkipVerifyTLS bool) Option { + return func(opts *options) { + opts.insecureSkipVerifyTLS = insecureSkipVerifyTLS + } +} + // WithTLSClientConfig sets the client auth with the provided credentials. func WithTLSClientConfig(certFile, keyFile, caFile string) Option { return func(opts *options) { diff --git a/pkg/getter/httpgetter.go b/pkg/getter/httpgetter.go index 5b476ff2d..695a87743 100644 --- a/pkg/getter/httpgetter.go +++ b/pkg/getter/httpgetter.go @@ -17,6 +17,7 @@ package getter import ( "bytes" + "crypto/tls" "io" "net/http" @@ -111,5 +112,19 @@ func (g *HTTPGetter) httpClient() (*http.Client, error) { return client, nil } + + if g.opts.insecureSkipVerifyTLS { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + Proxy: http.ProxyFromEnvironment, + }, + } + + return client, nil + } + return http.DefaultClient, nil } diff --git a/pkg/getter/httpgetter_test.go b/pkg/getter/httpgetter_test.go index b20085574..a1288bf47 100644 --- a/pkg/getter/httpgetter_test.go +++ b/pkg/getter/httpgetter_test.go @@ -44,12 +44,14 @@ func TestHTTPGetter(t *testing.T) { cd := "../../testdata" join := filepath.Join ca, pub, priv := join(cd, "rootca.crt"), join(cd, "crt.pem"), join(cd, "key.pem") + insecure := false // Test with options g, err = NewHTTPGetter( WithBasicAuth("I", "Am"), WithUserAgent("Groot"), WithTLSClientConfig(pub, priv, ca), + WithInsecureSkipVerifyTLS(insecure), ) if err != nil { t.Fatal(err) @@ -83,6 +85,29 @@ func TestHTTPGetter(t *testing.T) { if hg.opts.caFile != ca { t.Errorf("Expected NewHTTPGetter to contain %q as the CA file, got %q", ca, hg.opts.caFile) } + + if hg.opts.insecureSkipVerifyTLS != insecure { + t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", false, hg.opts.insecureSkipVerifyTLS) + } + + // Test if setting insecureSkipVerifyTLS is being passed to the ops + insecure = true + + g, err = NewHTTPGetter( + WithInsecureSkipVerifyTLS(insecure), + ) + if err != nil { + t.Fatal(err) + } + + hg, ok = g.(*HTTPGetter) + if !ok { + t.Fatal("expected NewHTTPGetter to produce an *HTTPGetter") + } + + if hg.opts.insecureSkipVerifyTLS != insecure { + t.Errorf("Expected NewHTTPGetter to contain %t as InsecureSkipVerifyTLs flag, got %t", insecure, hg.opts.insecureSkipVerifyTLS) + } } func TestDownload(t *testing.T) { @@ -191,3 +216,35 @@ func TestDownloadTLS(t *testing.T) { t.Error(err) } } + +func TestDownloadInsecureSkipTLSVerify(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer ts.Close() + + u, _ := url.ParseRequestURI(ts.URL) + + // Ensure the default behaviour did not change + g, err := NewHTTPGetter( + WithURL(u.String()), + ) + if err != nil { + t.Error(err) + } + + if _, err := g.Get(u.String()); err == nil { + t.Errorf("Expected Getter to throw an error, got %s", err) + } + + // Test certificate check skip + g, err = NewHTTPGetter( + WithURL(u.String()), + WithInsecureSkipVerifyTLS(true), + ) + if err != nil { + t.Error(err) + } + if _, err = g.Get(u.String()); err != nil { + t.Error(err) + } + +} diff --git a/pkg/helmpath/home_windows_test.go b/pkg/helmpath/home_windows_test.go index af74558a8..796ced62c 100644 --- a/pkg/helmpath/home_windows_test.go +++ b/pkg/helmpath/home_windows_test.go @@ -23,9 +23,9 @@ import ( ) func TestHelmHome(t *testing.T) { - os.Setenv(xdg.XDGCacheHomeEnvVar, "c:\\") - os.Setenv(xdg.XDGConfigHomeEnvVar, "d:\\") - os.Setenv(xdg.XDGDataHomeEnvVar, "e:\\") + os.Setenv(xdg.CacheHomeEnvVar, "c:\\") + os.Setenv(xdg.ConfigHomeEnvVar, "d:\\") + os.Setenv(xdg.DataHomeEnvVar, "e:\\") isEq := func(t *testing.T, a, b string) { if a != b { t.Errorf("Expected %q, got %q", b, a) diff --git a/pkg/helmpath/lazypath_windows_test.go b/pkg/helmpath/lazypath_windows_test.go index d02a0a7f6..866e7b9d9 100644 --- a/pkg/helmpath/lazypath_windows_test.go +++ b/pkg/helmpath/lazypath_windows_test.go @@ -32,7 +32,7 @@ const ( ) func TestDataPath(t *testing.T) { - os.Unsetenv(DataHomeEnvVar) + os.Unsetenv(xdg.DataHomeEnvVar) os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo")) expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) @@ -41,7 +41,7 @@ func TestDataPath(t *testing.T) { t.Errorf("expected '%s', got '%s'", expected, lazy.dataPath(testFile)) } - os.Setenv(DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) + os.Setenv(xdg.DataHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) @@ -70,8 +70,8 @@ func TestConfigPath(t *testing.T) { } func TestCachePath(t *testing.T) { - os.Unsetenv(CacheHomeEnvVar) - os.Setenv("APPDATA", filepath.Join(homedir.HomeDir(), "foo")) + os.Unsetenv(xdg.CacheHomeEnvVar) + os.Setenv("TEMP", filepath.Join(homedir.HomeDir(), "foo")) expected := filepath.Join(homedir.HomeDir(), "foo", appName, testFile) @@ -79,7 +79,7 @@ func TestCachePath(t *testing.T) { t.Errorf("expected '%s', got '%s'", expected, lazy.cachePath(testFile)) } - os.Setenv(CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) + os.Setenv(xdg.CacheHomeEnvVar, filepath.Join(homedir.HomeDir(), "xdg")) expected = filepath.Join(homedir.HomeDir(), "xdg", appName, testFile) diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 215fc8308..b761c6d12 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -33,6 +33,7 @@ import ( apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/runtime" @@ -50,6 +51,8 @@ import ( // ErrNoObjectsVisited indicates that during a visit operation, no matching objects were found. var ErrNoObjectsVisited = errors.New("no objects visited") +var metadataAccessor = meta.NewAccessor() + // Client represents a client capable of communicating with the Kubernetes API. type Client struct { Factory Factory @@ -88,7 +91,7 @@ func (c *Client) IsReachable() error { client, _ := c.Factory.KubernetesClientSet() _, err := client.ServerVersion() if err != nil { - return errors.New("Kubernetes cluster unreachable") + return fmt.Errorf("Kubernetes cluster unreachable: %s", err.Error()) } return nil } @@ -209,12 +212,20 @@ func (c *Client) Update(original, target ResourceList, force bool) (*Result, err } for _, info := range original.Difference(target) { - if info.Mapping.GroupVersionKind.Kind == "CustomResourceDefinition" { - c.Log("Skipping the deletion of CustomResourceDefinition %q", info.Name) + c.Log("Deleting %q in %s...", info.Name, info.Namespace) + + if err := info.Get(); err != nil { + c.Log("Unable to get obj %q, err: %s", info.Name, err) + } + annotations, err := metadataAccessor.Annotations(info.Object) + if err != nil { + c.Log("Unable to get annotations on %q, err: %s", info.Name, err) + } + if annotations != nil && annotations[ResourcePolicyAnno] == KeepPolicy { + c.Log("Skipping delete of %q due to annotation [%s=%s]", info.Name, ResourcePolicyAnno, KeepPolicy) continue } - c.Log("Deleting %q in %s...", info.Name, info.Namespace) res.Deleted = append(res.Deleted, info) if err := deleteResource(info); err != nil { if apierrors.IsNotFound(err) { @@ -236,11 +247,6 @@ func (c *Client) Delete(resources ResourceList) (*Result, []error) { var errs []error res := &Result{} err := perform(resources, func(info *resource.Info) error { - if info.Mapping.GroupVersionKind.Kind == "CustomResourceDefinition" { - c.Log("Skipping the deletion of CustomResourceDefinition %q", info.Name) - return nil - } - c.Log("Starting delete for %q %s", info.Name, info.Mapping.GroupVersionKind.Kind) if err := c.skipIfNotFound(deleteResource(info)); err != nil { // Collect the error and continue on diff --git a/pkg/kube/client_test.go b/pkg/kube/client_test.go index 9e7581d00..aa081423c 100644 --- a/pkg/kube/client_test.go +++ b/pkg/kube/client_test.go @@ -147,6 +147,8 @@ func TestUpdate(t *testing.T) { return newResponse(200, &listB.Items[1]) case p == "/namespaces/default/pods/squid" && m == "DELETE": return newResponse(200, &listB.Items[1]) + case p == "/namespaces/default/pods/squid" && m == "GET": + return newResponse(200, &listB.Items[2]) default: t.Fatalf("unexpected request: %s %s", req.Method, req.URL.Path) return nil, nil @@ -184,6 +186,7 @@ func TestUpdate(t *testing.T) { "/namespaces/default/pods/otter:GET", "/namespaces/default/pods/dolphin:GET", "/namespaces/default/pods:POST", + "/namespaces/default/pods/squid:GET", "/namespaces/default/pods/squid:DELETE", } if len(expectedActions) != len(actions) { diff --git a/pkg/kube/converter.go b/pkg/kube/converter.go index a5a6ae1f7..3bf0e358c 100644 --- a/pkg/kube/converter.go +++ b/pkg/kube/converter.go @@ -17,6 +17,10 @@ limitations under the License. package kube // import "helm.sh/helm/v3/pkg/kube" import ( + "sync" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -24,6 +28,9 @@ import ( "k8s.io/client-go/kubernetes/scheme" ) +var k8sNativeScheme *runtime.Scheme +var k8sNativeSchemeOnce sync.Once + // AsVersioned converts the given info into a runtime.Object with the correct // group and version set func AsVersioned(info *resource.Info) runtime.Object { @@ -33,12 +40,30 @@ func AsVersioned(info *resource.Info) runtime.Object { // convertWithMapper converts the given object with the optional provided // RESTMapping. If no mapping is provided, the default schema versioner is used func convertWithMapper(obj runtime.Object, mapping *meta.RESTMapping) runtime.Object { - var gv = runtime.GroupVersioner(schema.GroupVersions(scheme.Scheme.PrioritizedVersionsAllGroups())) + s := kubernetesNativeScheme() + var gv = runtime.GroupVersioner(schema.GroupVersions(s.PrioritizedVersionsAllGroups())) if mapping != nil { gv = mapping.GroupVersionKind.GroupVersion() } - if obj, err := runtime.ObjectConvertor(scheme.Scheme).ConvertToVersion(obj, gv); err == nil { + if obj, err := runtime.ObjectConvertor(s).ConvertToVersion(obj, gv); err == nil { return obj } return obj } + +// kubernetesNativeScheme returns a clean *runtime.Scheme with _only_ Kubernetes +// native resources added to it. This is required to break free of custom resources +// that may have been added to scheme.Scheme due to Helm being used as a package in +// combination with e.g. a versioned kube client. If we would not do this, the client +// may attempt to perform e.g. a 3-way-merge strategy patch for custom resources. +func kubernetesNativeScheme() *runtime.Scheme { + k8sNativeSchemeOnce.Do(func() { + k8sNativeScheme = runtime.NewScheme() + scheme.AddToScheme(k8sNativeScheme) + // API extensions are not in the above scheme set, + // and must thus be added separately. + apiextensionsv1beta1.AddToScheme(k8sNativeScheme) + apiextensionsv1.AddToScheme(k8sNativeScheme) + }) + return k8sNativeScheme +} diff --git a/pkg/kube/resource_policy.go b/pkg/kube/resource_policy.go new file mode 100644 index 000000000..5f391eb50 --- /dev/null +++ b/pkg/kube/resource_policy.go @@ -0,0 +1,26 @@ +/* +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 kube // import "helm.sh/helm/v3/pkg/kube" + +// ResourcePolicyAnno is the annotation name for a resource policy +const ResourcePolicyAnno = "helm.sh/resource-policy" + +// KeepPolicy is the resource policy type for keep +// +// This resource policy type allows resources to skip being deleted +// during an uninstallRelease action. +const KeepPolicy = "keep" diff --git a/pkg/kube/wait.go b/pkg/kube/wait.go index f0005a61e..0254a60bb 100644 --- a/pkg/kube/wait.go +++ b/pkg/kube/wait.go @@ -188,12 +188,25 @@ func (w *waiter) serviceReady(s *corev1.Service) bool { } // Make sure the service is not explicitly set to "None" before checking the IP - if (s.Spec.ClusterIP != corev1.ClusterIPNone && s.Spec.ClusterIP == "") || - // This checks if the service has a LoadBalancer and that balancer has an Ingress defined - (s.Spec.Type == corev1.ServiceTypeLoadBalancer && s.Status.LoadBalancer.Ingress == nil) { - w.log("Service does not have IP address: %s/%s", s.GetNamespace(), s.GetName()) + if s.Spec.ClusterIP != corev1.ClusterIPNone && s.Spec.ClusterIP == "" { + w.log("Service does not have cluster IP address: %s/%s", s.GetNamespace(), s.GetName()) return false } + + // This checks if the service has a LoadBalancer and that balancer has an Ingress defined + if s.Spec.Type == corev1.ServiceTypeLoadBalancer { + // do not wait when at least 1 external IP is set + if len(s.Spec.ExternalIPs) > 0 { + w.log("Service %s/%s has external IP addresses (%v), marking as ready", s.GetNamespace(), s.GetName(), s.Spec.ExternalIPs) + return true + } + + if s.Status.LoadBalancer.Ingress == nil { + w.log("Service does not have load balancer ingress IP address: %s/%s", s.GetNamespace(), s.GetName()) + return false + } + } + return true } diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index ac650ba57..772b1d064 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -97,7 +97,7 @@ func TestBadValues(t *testing.T) { if len(m) < 1 { t.Fatalf("All didn't fail with expected errors, got %#v", m) } - if !strings.Contains(m[0].Err.Error(), "cannot unmarshal") { + if !strings.Contains(m[0].Err.Error(), "unable to parse YAML") { t.Errorf("All didn't have the error for invalid key format: %s", m[0].Err) } } diff --git a/pkg/lint/rules/template.go b/pkg/lint/rules/template.go index ce8b2ded9..49f5eb697 100644 --- a/pkg/lint/rules/template.go +++ b/pkg/lint/rules/template.go @@ -126,11 +126,9 @@ func Templates(linter *support.Linter, values map[string]interface{}, namespace // key will be raised as well err := yaml.Unmarshal([]byte(renderedContent), &yamlStruct) - validYaml := linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err)) - - if !validYaml { - continue - } + // If YAML linting fails, we sill progress. So we don't capture the returned state + // on this linter run. + linter.RunLinterRule(support.ErrorSev, path, validateYamlContent(err)) } } diff --git a/pkg/lint/rules/values_test.go b/pkg/lint/rules/values_test.go new file mode 100644 index 000000000..901a739fd --- /dev/null +++ b/pkg/lint/rules/values_test.go @@ -0,0 +1,37 @@ +/* +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 rules + +import ( + "os" + "path/filepath" + "testing" +) + +var ( + nonExistingValuesFilePath = filepath.Join("/fake/dir", "values.yaml") +) + +func TestValidateValuesYamlNotDirectory(t *testing.T) { + _ = os.Mkdir(nonExistingValuesFilePath, os.ModePerm) + defer os.Remove(nonExistingValuesFilePath) + + err := validateValuesFileExistence(nonExistingValuesFilePath) + if err == nil { + t.Errorf("validateValuesFileExistence to return a linter error, got no error") + } +} diff --git a/pkg/releaseutil/sorter_test.go b/pkg/releaseutil/sorter_test.go index 69a6543ad..9544d2018 100644 --- a/pkg/releaseutil/sorter_test.go +++ b/pkg/releaseutil/sorter_test.go @@ -79,3 +79,30 @@ func TestSortByRevision(t *testing.T) { return vi < vj }) } + +func TestReverseSortByName(t *testing.T) { + Reverse(releases, SortByName) + check(t, "ByName", func(i, j int) bool { + ni := releases[i].Name + nj := releases[j].Name + return ni > nj + }) +} + +func TestReverseSortByDate(t *testing.T) { + Reverse(releases, SortByDate) + check(t, "ByDate", func(i, j int) bool { + ti := releases[i].Info.LastDeployed.Second() + tj := releases[j].Info.LastDeployed.Second() + return ti > tj + }) +} + +func TestReverseSortByRevision(t *testing.T) { + Reverse(releases, SortByRevision) + check(t, "ByRevision", func(i, j int) bool { + vi := releases[i].Version + vj := releases[j].Version + return vi > vj + }) +} diff --git a/pkg/repo/chartrepo.go b/pkg/repo/chartrepo.go index 38b6b8fb0..c2c366a1e 100644 --- a/pkg/repo/chartrepo.go +++ b/pkg/repo/chartrepo.go @@ -19,8 +19,10 @@ package repo // import "helm.sh/helm/v3/pkg/repo" import ( "crypto/rand" "encoding/base64" + "encoding/json" "fmt" "io/ioutil" + "log" "net/url" "os" "path" @@ -38,13 +40,14 @@ import ( // Entry represents a collection of parameters for chart repository type Entry struct { - Name string `json:"name"` - URL string `json:"url"` - Username string `json:"username"` - Password string `json:"password"` - CertFile string `json:"certFile"` - KeyFile string `json:"keyFile"` - CAFile string `json:"caFile"` + Name string `json:"name"` + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password"` + CertFile string `json:"certFile"` + KeyFile string `json:"keyFile"` + CAFile string `json:"caFile"` + InsecureSkipTLSverify bool `json:"insecure_skip_tls_verify"` } // ChartRepository represents a chart repository @@ -121,6 +124,7 @@ func (r *ChartRepository) DownloadIndexFile() (string, error) { // TODO add user-agent resp, err := r.Client.Get(indexURL, getter.WithURL(r.Config.URL), + getter.WithInsecureSkipVerifyTLS(r.Config.InsecureSkipTLSverify), getter.WithTLSClientConfig(r.Config.CertFile, r.Config.KeyFile, r.Config.CAFile), getter.WithBasicAuth(r.Config.Username, r.Config.Password), ) @@ -271,3 +275,11 @@ func ResolveReferenceURL(baseURL, refURL string) (string, error) { parsedBaseURL.Path = strings.TrimSuffix(parsedBaseURL.Path, "/") + "/" return parsedBaseURL.ResolveReference(parsedRefURL).String(), nil } + +func (e *Entry) String() string { + buf, err := json.Marshal(e) + if err != nil { + log.Panic(err) + } + return string(buf) +} diff --git a/pkg/repo/repotest/server.go b/pkg/repo/repotest/server.go index 96a8bbfcc..b18bce49c 100644 --- a/pkg/repo/repotest/server.go +++ b/pkg/repo/repotest/server.go @@ -22,6 +22,8 @@ import ( "os" "path/filepath" + "helm.sh/helm/v3/internal/tlsutil" + "sigs.k8s.io/yaml" "helm.sh/helm/v3/pkg/repo" @@ -143,6 +145,40 @@ func (s *Server) Start() { })) } +func (s *Server) StartTLS() { + cd := "../../testdata" + ca, pub, priv := filepath.Join(cd, "rootca.crt"), filepath.Join(cd, "crt.pem"), filepath.Join(cd, "key.pem") + + s.srv = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if s.middleware != nil { + s.middleware.ServeHTTP(w, r) + } + http.FileServer(http.Dir(s.Root())).ServeHTTP(w, r) + })) + tlsConf, err := tlsutil.NewClientTLS(pub, priv, ca) + if err != nil { + panic(err) + } + tlsConf.BuildNameToCertificate() + tlsConf.ServerName = "helm.sh" + s.srv.TLS = tlsConf + s.srv.StartTLS() + + // Set up repositories config with ca file + repoConfig := filepath.Join(s.Root(), "repositories.yaml") + + r := repo.NewFile() + r.Add(&repo.Entry{ + Name: "test", + URL: s.URL(), + CAFile: filepath.Join("../../testdata", "rootca.crt"), + }) + + if err := r.WriteFile(repoConfig, 0644); err != nil { + panic(err) + } +} + // Stop stops the server and closes all connections. // // It should be called explicitly. diff --git a/pkg/storage/driver/cfgmaps.go b/pkg/storage/driver/cfgmaps.go index cc2e2416a..f9d4da8c3 100644 --- a/pkg/storage/driver/cfgmaps.go +++ b/pkg/storage/driver/cfgmaps.go @@ -201,11 +201,6 @@ func (cfgmaps *ConfigMaps) Update(key string, rls *rspb.Release) error { func (cfgmaps *ConfigMaps) Delete(key string) (rls *rspb.Release, err error) { // fetch the release to check existence if rls, err = cfgmaps.Get(key); err != nil { - if apierrors.IsNotFound(err) { - return nil, ErrReleaseExists - } - - cfgmaps.Log("delete: failed to get release %q: %s", key, err) return nil, err } // delete the release diff --git a/pkg/storage/driver/cfgmaps_test.go b/pkg/storage/driver/cfgmaps_test.go index 2aa38f284..e40247d3c 100644 --- a/pkg/storage/driver/cfgmaps_test.go +++ b/pkg/storage/driver/cfgmaps_test.go @@ -184,3 +184,34 @@ func TestConfigMapUpdate(t *testing.T) { t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String()) } } + +func TestConfigMapDelete(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + cfgmaps := newTestFixtureCfgMaps(t, []*rspb.Release{rel}...) + + // perform the delete on a non-existent release + _, err := cfgmaps.Delete("nonexistent") + if err != ErrReleaseNotFound { + t.Fatalf("Expected ErrReleaseNotFound: got {%v}", err) + } + + // perform the delete + rls, err := cfgmaps.Delete(key) + if err != nil { + t.Fatalf("Failed to delete release with key %q: %s", key, err) + } + if !reflect.DeepEqual(rel, rls) { + t.Errorf("Expected {%v}, got {%v}", rel, rls) + } + + // fetch the deleted release + _, err = cfgmaps.Get(key) + if !reflect.DeepEqual(ErrReleaseNotFound, err) { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} diff --git a/pkg/storage/driver/records_test.go b/pkg/storage/driver/records_test.go index 79b60044f..0a27839cc 100644 --- a/pkg/storage/driver/records_test.go +++ b/pkg/storage/driver/records_test.go @@ -17,6 +17,7 @@ limitations under the License. package driver // import "helm.sh/helm/v3/pkg/storage/driver" import ( + "reflect" "testing" rspb "helm.sh/helm/v3/pkg/release" @@ -110,3 +111,130 @@ func TestRecordsRemoveAt(t *testing.T) { t.Fatalf("Expected length of rs to be 1, got %d", len(rs)) } } + +func TestRecordsGet(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + rec *record + }{ + { + "get valid key", + "rls-a.v1", + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + }, + { + "get invalid key", + "rls-a.v3", + nil, + }, + } + + for _, tt := range tests { + got := rs.Get(tt.key) + if !reflect.DeepEqual(tt.rec, got) { + t.Fatalf("Expected %v, got %v", tt.rec, got) + } + } +} + +func TestRecordsIndex(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + sort int + }{ + { + "get valid key", + "rls-a.v1", + 0, + }, + { + "get invalid key", + "rls-a.v3", + -1, + }, + } + + for _, tt := range tests { + got, _ := rs.Index(tt.key) + if got != tt.sort { + t.Fatalf("Expected %d, got %d", tt.sort, got) + } + } +} + +func TestRecordsExists(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + ok bool + }{ + { + "get valid key", + "rls-a.v1", + true, + }, + { + "get invalid key", + "rls-a.v3", + false, + }, + } + + for _, tt := range tests { + got := rs.Exists(tt.key) + if got != tt.ok { + t.Fatalf("Expected %t, got %t", tt.ok, got) + } + } +} + +func TestRecordsReplace(t *testing.T) { + rs := records([]*record{ + newRecord("rls-a.v1", releaseStub("rls-a", 1, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }) + + var tests = []struct { + desc string + key string + rec *record + expected *record + }{ + { + "replace with existing key", + "rls-a.v2", + newRecord("rls-a.v3", releaseStub("rls-a", 3, "default", rspb.StatusSuperseded)), + newRecord("rls-a.v2", releaseStub("rls-a", 2, "default", rspb.StatusDeployed)), + }, + { + "replace with non existing key", + "rls-a.v4", + newRecord("rls-a.v4", releaseStub("rls-a", 4, "default", rspb.StatusDeployed)), + nil, + }, + } + + for _, tt := range tests { + got := rs.Replace(tt.key, tt.rec) + if !reflect.DeepEqual(tt.expected, got) { + t.Fatalf("Expected %v, got %v", tt.expected, got) + } + } +} diff --git a/pkg/storage/driver/secrets.go b/pkg/storage/driver/secrets.go index dcb2ecfcf..1f1931aed 100644 --- a/pkg/storage/driver/secrets.go +++ b/pkg/storage/driver/secrets.go @@ -185,11 +185,7 @@ func (secrets *Secrets) Update(key string, rls *rspb.Release) error { func (secrets *Secrets) Delete(key string) (rls *rspb.Release, err error) { // fetch the release to check existence if rls, err = secrets.Get(key); err != nil { - if apierrors.IsNotFound(err) { - return nil, ErrReleaseExists - } - - return nil, errors.Wrapf(err, "delete: failed to get release %q", key) + return nil, err } // delete the release err = secrets.impl.Delete(key, &metav1.DeleteOptions{}) diff --git a/pkg/storage/driver/secrets_test.go b/pkg/storage/driver/secrets_test.go index 892482e5b..5f0ecc8bb 100644 --- a/pkg/storage/driver/secrets_test.go +++ b/pkg/storage/driver/secrets_test.go @@ -184,3 +184,34 @@ func TestSecretUpdate(t *testing.T) { t.Errorf("Expected status %s, got status %s", rel.Info.Status.String(), got.Info.Status.String()) } } + +func TestSecretDelete(t *testing.T) { + vers := 1 + name := "smug-pigeon" + namespace := "default" + key := testKey(name, vers) + rel := releaseStub(name, vers, namespace, rspb.StatusDeployed) + + secrets := newTestFixtureSecrets(t, []*rspb.Release{rel}...) + + // perform the delete on a non-existing release + _, err := secrets.Delete("nonexistent") + if err != ErrReleaseNotFound { + t.Fatalf("Expected ErrReleaseNotFound, got: {%v}", err) + } + + // perform the delete + rls, err := secrets.Delete(key) + if err != nil { + t.Fatalf("Failed to delete release with key %q: %s", key, err) + } + if !reflect.DeepEqual(rel, rls) { + t.Errorf("Expected {%v}, got {%v}", rel, rls) + } + + // fetch the deleted release + _, err = secrets.Get(key) + if !reflect.DeepEqual(ErrReleaseNotFound, err) { + t.Errorf("Expected {%v}, got {%v}", ErrReleaseNotFound, err) + } +} diff --git a/pkg/strvals/parser_test.go b/pkg/strvals/parser_test.go index 7f38efa52..44d317220 100644 --- a/pkg/strvals/parser_test.go +++ b/pkg/strvals/parser_test.go @@ -449,6 +449,39 @@ func TestParseIntoString(t *testing.T) { } } +func TestParseFile(t *testing.T) { + input := "name1=path1" + expect := map[string]interface{}{ + "name1": "value1", + } + rs2v := func(rs []rune) (interface{}, error) { + v := string(rs) + if v != "path1" { + t.Errorf("%s: runesToVal: Expected value path1, got %s", input, v) + return "", nil + } + return "value1", nil + } + + got, err := ParseFile(input, rs2v) + if err != nil { + t.Fatal(err) + } + + y1, err := yaml.Marshal(expect) + if err != nil { + t.Fatal(err) + } + y2, err := yaml.Marshal(got) + if err != nil { + t.Fatalf("Error serializing parsed value: %s", err) + } + + if string(y1) != string(y2) { + t.Errorf("%s: Expected:\n%s\nGot:\n%s", input, y1, y2) + } +} + func TestParseIntoFile(t *testing.T) { got := map[string]interface{}{} input := "name1=path1" diff --git a/scripts/get b/scripts/get index 711635ee3..3da11d4a4 100755 --- a/scripts/get +++ b/scripts/get @@ -78,16 +78,14 @@ verifySupported() { # checkDesiredVersion checks if the desired version is available. checkDesiredVersion() { if [ "x$DESIRED_VERSION" == "x" ]; then - # FIXME(bacongobbler): hard code the desired version for the time being. - # A better fix would be to filter for Helm 2 release pages. - TAG="v2.16.1" # Get tag from release URL - # local latest_release_url="https://github.com/helm/helm/releases/latest" - # if type "curl" > /dev/null; then - # TAG=$(curl -Ls -o /dev/null -w %{url_effective} $latest_release_url | grep -oE "[^/]+$" ) - # elif type "wget" > /dev/null; then - # TAG=$(wget $latest_release_url --server-response -O /dev/null 2>&1 | awk '/^ Location: /{DEST=$2} END{ print DEST}' | grep -oE "[^/]+$") - # fi + local release_url="https://github.com/helm/helm/releases" + if type "curl" > /dev/null; then + + TAG=$(curl -Ls $release_url | grep 'href="/helm/helm/releases/tag/v2.' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') + elif type "wget" > /dev/null; then + TAG=$(wget $release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v2.' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') + fi else TAG=$DESIRED_VERSION fi diff --git a/scripts/get-helm-3 b/scripts/get-helm-3 index c1655a68e..a974d97b6 100755 --- a/scripts/get-helm-3 +++ b/scripts/get-helm-3 @@ -78,11 +78,11 @@ verifySupported() { checkDesiredVersion() { if [ "x$DESIRED_VERSION" == "x" ]; then # Get tag from release URL - local latest_release_url="https://github.com/helm/helm/releases/latest" + local latest_release_url="https://github.com/helm/helm/releases" if type "curl" > /dev/null; then - TAG=$(curl -Ls -o /dev/null -w %{url_effective} $latest_release_url | grep -oE "[^/]+$" ) + TAG=$(curl -Ls $latest_release_url | grep 'href="/helm/helm/releases/tag/v3.' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') elif type "wget" > /dev/null; then - TAG=$(wget $latest_release_url --server-response -O /dev/null 2>&1 | awk '/^ Location: /{DEST=$2} END{ print DEST}' | grep -oE "[^/]+$") + TAG=$(wget $latest_release_url -O - 2>&1 | grep 'href="/helm/helm/releases/tag/v3.' | grep -v no-underline | head -n 1 | cut -d '"' -f 2 | awk '{n=split($NF,a,"/");print a[n]}' | awk 'a !~ $0{print}; {a=$0}') fi else TAG=$DESIRED_VERSION diff --git a/scripts/release-notes.sh b/scripts/release-notes.sh new file mode 100755 index 000000000..dd48d4a17 --- /dev/null +++ b/scripts/release-notes.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash + +# Copyright The Helm Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +RELEASE=${RELEASE:-$2} +PREVIOUS_RELEASE=${PREVIOUS_RELEASE:-$1} + +## Ensure Correct Usage +if [[ -z "${PREVIOUS_RELEASE}" || -z "${RELEASE}" ]]; then + echo Usage: + echo ./scripts/release-notes.sh v3.0.0 v3.1.0 + echo or + echo PREVIOUS_RELEASE=v3.0.0 + echo RELEASE=v3.1.0 + echo ./scripts/release-notes.sh + exit 1 +fi + +## validate git tags +for tag in $RELEASE $PREVIOUS_RELEASE; do + OK=$(git tag -l ${tag} | wc -l) + if [[ "$OK" == "0" ]]; then + echo ${tag} is not a valid release version + exit 1 + fi +done + +## Check for hints that checksum files were downloaded +## from `make fetch-dist` +if [[ ! -e "./_dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256" ]]; then + echo "checksum file ./_dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256 not found in ./_dist/" + echo "Did you forget to run \`make fetch-dist\` first ?" + exit 1 +fi + +## Generate CHANGELOG from git log +CHANGELOG=$(git log --no-merges --pretty=format:'- %s %H (%aN)' ${PREVIOUS_RELEASE}..${RELEASE}) +if [[ ! $? -eq 0 ]]; then + echo "Error creating changelog" + echo "try running \`git log --no-merges --pretty=format:'- %s %H (%aN)' ${PREVIOUS_RELEASE}..${RELEASE}\`" + exit 1 +fi + +## guess at MAJOR / MINOR / PATCH versions +MAJOR=$(echo ${RELEASE} | sed 's/^v//' | cut -f1 -d.) +MINOR=$(echo ${RELEASE} | sed 's/^v//' | cut -f2 -d.) +PATCH=$(echo ${RELEASE} | sed 's/^v//' | cut -f3 -d.) + +## Print release notes to stdout +cat <. Users are encouraged to upgrade for the best experience. + +The community keeps growing, and we'd love to see you there! + +- Join the discussion in [Kubernetes Slack](https://kubernetes.slack.com): + - `#helm-users` for questions and just to hang out + - `#helm-dev` for discussing PRs, code, and bugs +- Hang out at the Public Developer Call: Thursday, 9:30 Pacific via [Zoom](https://zoom.us/j/696660622) +- Test, debug, and contribute charts: [GitHub/helm/charts](https://github.com/helm/charts) + +## Notable Changes + +- Add list of +- notable changes here + +## Installation and Upgrading + +Download Helm ${RELEASE}. The common platform binaries are here: + +- [MacOS amd64](https://get.helm.sh/helm-${RELEASE}-darwin-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-darwin-amd64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256)) +- [Linux amd64](https://get.helm.sh/helm-${RELEASE}-linux-amd64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-amd64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-amd64.tar.gz.sha256)) +- [Linux arm](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm.tar.gz.sha256)) +- [Linux arm64](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-arm64.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-arm64.tar.gz.sha256)) +- [Linux i386](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-386.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-386.tar.gz.sha256)) +- [Linux ppc64le](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-linux-ppc64le.tar.gz.sha256)) +- [Linux s390x](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz) ([checksum](https://get.helm.sh/helm-${RELEASE}-linux-s390x.tar.gz.sha256sum) / $(cat _dist/helm-${RELEASE}-darwin-amd64.tar.gz.sha256)) +- [Windows amd64](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip) ([checksum](https://get.helm.sh/helm-${RELEASE}-windows-amd64.zip.sha256sum) / $(cat _dist/helm-${RELEASE}-windows-amd64.zip.sha256)) + +The [Quickstart Guide](https://docs.helm.sh/using_helm/#quickstart-guide) will get you going from there. For **upgrade instructions** or detailed installation notes, check the [install guide](https://docs.helm.sh/using_helm/#installing-helm). You can also use a [script to install](https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3) on any system with \`bash\`. + +## What's Next + +- ${MAJOR}.${MINOR}.$(expr ${PATCH} + 1) will contain only bug fixes. +- ${MAJOR}.$(expr ${MINOR} + 1).${PATCH} is the next feature release. This release will focus on ... + +## Changelog + +${CHANGELOG} +EOF diff --git a/testdata/releases.yaml b/testdata/releases.yaml new file mode 100644 index 000000000..fef79f424 --- /dev/null +++ b/testdata/releases.yaml @@ -0,0 +1,43 @@ +# This file can be used as input to create test releases: +# HELM_MEMORY_DRIVER_DATA=./testdata/releases.yaml HELM_DRIVER=memory helm list --all-namespaces +- name: athos + version: 1 + namespace: default + info: + status: deployed + chart: + metadata: + name: athos-chart + version: 1.0.0 + appversion: 1.1.0 +- name: porthos + version: 2 + namespace: default + info: + status: deployed + chart: + metadata: + name: prothos-chart + version: 0.2.0 + appversion: 0.2.2 +- name: aramis + version: 3 + namespace: default + info: + status: deployed + chart: + metadata: + name: aramis-chart + version: 0.0.3 + appversion: 3.0.3 +- name: dartagnan + version: 4 + namespace: gascony + info: + status: deployed + chart: + metadata: + name: dartagnan-chart + version: 0.4.4 + appversion: 4.4.4 +